"أداة التبديل بين أجهزة التشغيل" هي ميزة في حزمة تطوير البرامج (SDK) لتقنية Cast تتيح النقل السلس للمحتوى بين التشغيل المحلي والبعيد عن بُعد بدءًا من الإصدار Android 13. والهدف من ذلك هو مساعدة تطبيقات المرسل على التحكم بسهولة وسرعة في مكان تشغيل المحتوى.
"أداة التبديل بين أجهزة التشغيل" تستخدم مكتبة
MediaRouter
لتبديل تشغيل المحتوى بين مكبِّر صوت الهاتف والأجهزة المقترنة التي تتضمن بلوتوث والأجهزة التي تعمل عن بُعد والتي تعمل بتكنولوجيا Google Cast. يمكن تقسيم حالات الاستخدام إلى
السيناريوهات التالية:
قم بتنزيل النموذج أدناه واستخدمه كمرجع حول كيفية تنفيذ مُبدل الإخراج في تطبيق الصوت. ويمكنك الاطلاع على README.md للحصول على إرشادات بشأن كيفية تشغيل النموذج.
يجب أن تكون "أداة التبديل بين أجهزة التشغيل" مُفعَّلة لإتاحة عمليات الاتصال عن بُعد أو عمليات الاتصال عن بُعد على المستوى المحلي باستخدام الخطوات التي يتناولها هذا الدليل. ليس هناك خطوات إضافية مطلوبة لإتاحة النقل بين مكبّرات صوت الجهاز المحلي وأجهزة بلوتوث المقترنة.
تطبيقات الصوت هي تطبيقات تتوافق مع Google Cast for Audio في إعدادات تطبيق الجهاز الاستقبال في وحدة تحكُّم مطوّري برامج Google Cast SDK.
واجهة مستخدم "أداة تبديل الإخراج"
تعرض "أداة التبديل بين أجهزة التشغيل" الأجهزة المحلية والبعيدة المتوفرة، بالإضافة إلى حالات الأجهزة الحالية، بما في ذلك اتصال الجهاز وحالته، إذا كان الجهاز محددًا. إذا كانت هناك أجهزة أخرى بالإضافة إلى الجهاز الحالي، يتيح لك النقر على جهاز آخر نقل تشغيل الوسائط إلى الجهاز المحدد.
المشاكل المعروفة
- سيتم إغلاق جلسات الوسائط التي تم إنشاؤها للتشغيل المحلي وإعادة إنشائها عند التبديل إلى إشعار حزمة تطوير البرامج (SDK) الخاصة بالبث.
نقاط الإدخال
إشعار الوسائط
إذا نشر تطبيق ما إشعار وسائط باستخدام
MediaSession
للتشغيل المحلي (يتم التشغيل محليًا)،
يعرض أعلى يسار إشعار الوسائط
شريحة إشعار بها اسم الجهاز (مثل مكبّر صوت الهاتف)
التي يتم تشغيل المحتوى عليها حاليًا. يؤدي النقر على شريحة الإشعارات إلى فتح واجهة
المستخدم لنظام مربع الحوار "أداة تبديل الإخراج".
إعدادات مستوى الصوت
يمكن أيضًا تشغيل واجهة مستخدم نظام مربّع حوار "أداة تبديل الإخراج" من خلال النقر على أزرار مستوى الصوت الفعلية على الجهاز، والنقر على رمز الإعدادات في أسفل الشاشة، ثم النقر على نص "تشغيل <اسم التطبيق> على <جهاز البث>".
ملخّص الخطوات
- التأكُّد من استيفاء المتطلبات الأساسية
- تفعيل "أداة التبديل بين أجهزة التشغيل" في AndroidManifest.xml
- تعديل SessionManagerListener للبث في الخلفية
- ضبط علامة setRemoteToLocalEnabled
- متابعة التشغيل على الجهاز
المتطلّبات الأساسية
- يمكنك ترحيل تطبيق Android الحالي إلى AndroidX.
- يُرجى تحديث
build.gradle
في تطبيقك لاستخدام الحدّ الأدنى المطلوب من "حزمة تطوير البرامج (SDK) للمرسِل بواسطة Android" من أجل "أداة التبديل بين أجهزة التشغيل":dependencies { ... implementation 'com.google.android.gms:play-services-cast-framework:21.2.0' ... }
- يتيح التطبيق إشعارات الوسائط.
- جهاز يعمل بنظام التشغيل Android 13
إعداد إشعارات الوسائط
لاستخدام "أداة التبديل بين أجهزة التشغيل"، يجب أن تنشئ تطبيقات الصوت والفيديو
إشعارًا بالوسائط لعرض حالة التشغيل وعناصر التحكم في الوسائط الخاصة بها للتشغيل المحلي. يتطلّب ذلك إنشاء MediaSession
، وضبط MediaStyle
باستخدام الرمز المميّز MediaSession
، وضبط عناصر التحكّم في الوسائط على الإشعار.
إذا كنت لا تستخدم حاليًا MediaStyle
وMediaSession
، يعرض المقتطف أدناه كيفية إعدادهما وتتوفّر أدلة لإعداد عمليات الاستدعاء لجلسات الوسائط لتطبيقات الصوت والفيديو:
// Create a media session. NotificationCompat.MediaStyle // PlayerService is your own Service or Activity responsible for media playback. val mediaSession = MediaSessionCompat(this, "PlayerService") // Create a MediaStyle object and supply your media session token to it. val mediaStyle = Notification.MediaStyle().setMediaSession(mediaSession.sessionToken) // Create a Notification which is styled by your MediaStyle object. // This connects your media session to the media controls. // Don't forget to include a small icon. val notification = Notification.Builder(this@PlayerService, CHANNEL_ID) .setStyle(mediaStyle) .setSmallIcon(R.drawable.ic_app_logo) .build() // Specify any actions which your users can perform, such as pausing and skipping to the next track. val pauseAction: Notification.Action = Notification.Action.Builder( pauseIcon, "Pause", pauseIntent ).build() notification.addAction(pauseAction)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { // Create a media session. NotificationCompat.MediaStyle // PlayerService is your own Service or Activity responsible for media playback. MediaSession mediaSession = new MediaSession(this, "PlayerService"); // Create a MediaStyle object and supply your media session token to it. Notification.MediaStyle mediaStyle = new Notification.MediaStyle().setMediaSession(mediaSession.getSessionToken()); // Specify any actions which your users can perform, such as pausing and skipping to the next track. Notification.Action pauseAction = Notification.Action.Builder(pauseIcon, "Pause", pauseIntent).build(); // Create a Notification which is styled by your MediaStyle object. // This connects your media session to the media controls. // Don't forget to include a small icon. String CHANNEL_ID = "CHANNEL_ID"; Notification notification = new Notification.Builder(this, CHANNEL_ID) .setStyle(mediaStyle) .setSmallIcon(R.drawable.ic_app_logo) .addAction(pauseAction) .build(); }
بالإضافة إلى ذلك، لتعبئة الإشعار بمعلومات الوسائط، يجب إضافة البيانات الوصفية وحالة التشغيل للوسائط إلى MediaSession
.
لإضافة بيانات وصفية إلى MediaSession
، يمكنك استخدام
setMetaData()
وتقديم جميع ثوابت MediaMetadata
المرتبطة بوسائطك في
MediaMetadataCompat.Builder()
.
mediaSession.setMetadata(MediaMetadataCompat.Builder() // Title .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title) // Artist // Could also be the channel name or TV series. .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist) // Album art // Could also be a screenshot or hero image for video content // The URI scheme needs to be "content", "file", or "android.resource". .putString( MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri) ) // Duration // If duration isn't set, such as for live broadcasts, then the progress // indicator won't be shown on the seekbar. .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration) .build() )
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { mediaSession.setMetadata( new MediaMetadataCompat.Builder() // Title .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title) // Artist // Could also be the channel name or TV series. .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist) // Album art // Could also be a screenshot or hero image for video content // The URI scheme needs to be "content", "file", or "android.resource". .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri) // Duration // If duration isn't set, such as for live broadcasts, then the progress // indicator won't be shown on the seekbar. .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration) .build() ); }
لإضافة حالة التشغيل إلى MediaSession
، يمكنك استخدام
setPlaybackState()
وتقديم جميع ثوابت PlaybackStateCompat
الخاصة بوسائطك في
PlaybackStateCompat.Builder()
.
mediaSession.setPlaybackState( PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, // Playback position // Used to update the elapsed time and the progress bar. mediaPlayer.currentPosition.toLong(), // Playback speed // Determines the rate at which the elapsed time changes. playbackSpeed ) // isSeekable // Adding the SEEK_TO action indicates that seeking is supported // and makes the seekbar position marker draggable. If this is not // supplied seek will be disabled but progress will still be shown. .setActions(PlaybackStateCompat.ACTION_SEEK_TO) .build() )
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { mediaSession.setPlaybackState( new PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, // Playback position // Used to update the elapsed time and the progress bar. mediaPlayer.currentPosition.toLong(), // Playback speed // Determines the rate at which the elapsed time changes. playbackSpeed ) // isSeekable // Adding the SEEK_TO action indicates that seeking is supported // and makes the seekbar position marker draggable. If this is not // supplied seek will be disabled but progress will still be shown. .setActions(PlaybackStateCompat.ACTION_SEEK_TO) .build() ); }
سلوك إشعارات تطبيق الفيديو
يجب أن تتبع تطبيقات الفيديو أو التطبيقات الصوتية التي لا تتيح التشغيل المحلي في الخلفية سلوكًا محددًا لإشعارات الوسائط لتجنّب المشاكل المتعلقة بإرسال أوامر الوسائط في الحالات التي لا يكون فيها التشغيل متاحًا:
- انشر إشعار الوسائط عند تشغيل الوسائط محليًا وكان التطبيق في المقدمة.
- إيقاف التشغيل المحلي مؤقتًا وإغلاق الإشعار عندما يكون التطبيق في الخلفية.
- عندما يعود التطبيق إلى المقدمة، يجب استئناف التشغيل على الجهاز وإعادة نشر الإشعار.
تفعيل "أداة التبديل بين الإخراج" في AndroidManifest.xml
لتفعيل "أداة التبديل بين أجهزة التشغيل"، يجب إضافة MediaTransferReceiver
إلى AndroidManifest.xml
في التطبيق. وإذا لم يكن مفعَّلاً، لن يتم تفعيل الميزة
ولن تكون علامة ميزة "الاتصال عن بعد إلى المنطقة المحلية" صالحة أيضًا.
<application>
...
<receiver
android:name="androidx.mediarouter.media.MediaTransferReceiver"
android:exported="true">
</receiver>
...
</application>
MediaTransferReceiver
هو مستقبِل بث يتيح نقل الوسائط بين الأجهزة من خلال واجهة مستخدم النظام. يُرجى الاطّلاع على مرجع
MediaTransferReceiver
للحصول على مزيد من المعلومات.
من المحلية إلى الاتصال عن بُعد
عندما يبدِّل المستخدم تشغيل المحتوى من الجهاز المحلي إلى جهاز التحكّم عن بُعد، ستبدأ حزمة تطوير البرامج (SDK) الخاصة بالبثّ تلقائيًا. ومع ذلك، يجب أن تتعامل التطبيقات مع التبديل من جهاز محلي إلى جهاز تحكّم عن بُعد، على سبيل المثال، إيقاف التشغيل المحلي وتحميل الوسائط على جهاز البث. يجب أن تستمع التطبيقات إلى ميزات البثّ
SessionManagerListener
،
باستخدام
onSessionStarted()
وonSessionEnded()
معاودة الاتصال، وتتعامل مع الإجراء عند تلقّي طلبات معاودة الاتصال
SessionManager
للإرسال. يجب أن تضمن التطبيقات أن هذه الاستدعاءات لا تزال نشطة عند
فتح مربع حوار "أداة التبديل بين أجهزة التشغيل" ولا يكون التطبيق في المقدّمة.
تعديل SessionManagerListener للبث في الخلفية
تتيح تجربة البث القديمة إمكانية الاتصال عن بُعد بين الأجهزة المحلية عند تشغيل التطبيق في المقدّمة. تبدأ تجربة "البث" النموذجية عندما ينقر المستخدمون على رمز
البثّ في التطبيق ويختارون جهازًا لبث الوسائط. في هذه الحالة، يجب على التطبيق التسجيل في
SessionManagerListener
،
في onCreate()
أو
onStart()
وإلغاء تسجيل المستمع في
onStop()
أو
onDestroy()
نشاط التطبيق.
بفضل التجربة الجديدة التي تتيح البث باستخدام "أداة التبديل بين أجهزة التشغيل"، يمكن للتطبيقات بدء البث عندما تعمل في الخلفية. وهذا مفيد بشكل خاص للتطبيقات الصوتية
التي تنشر إشعارات عند تشغيلها في الخلفية. يمكن للتطبيقات
تسجيل مستمعي SessionManager
في onCreate()
للخدمة
وإلغاء التسجيل في onDestroy()
للخدمة. وبهذه الطريقة، يجب أن تتلقّى التطبيقات دائمًا
طلبات معاودة الاتصال محليًا عن بُعد (مثل onSessionStarted
) عندما يكون
التطبيق يعمل في الخلفية.
إذا كان التطبيق يستخدم
MediaBrowserService
،
يُنصح بتسجيل SessionManagerListener
هناك.
class MyService : Service() { private var castContext: CastContext? = null protected fun onCreate() { castContext = CastContext.getSharedInstance(this) castContext .getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession::class.java) } protected fun onDestroy() { if (castContext != null) { castContext .getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession::class.java) } } }
public class MyService extends Service { private CastContext castContext; @Override protected void onCreate() { castContext = CastContext.getSharedInstance(this); castContext .getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession.class); } @Override protected void onDestroy() { if (castContext != null) { castContext .getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession.class); } } }
بفضل هذا التحديث، تعمل تقنية الاتصال عن بُعد بالطريقة نفسها التي تتّبعها في البث التقليدي عندما يكون التطبيق في الخلفية، وليس عليك اتّخاذ أي إجراءات إضافية للتبديل من أجهزة تتضمّن بلوتوث إلى أجهزة البث.
الوصول عن بعد إلى المناطق المحلية
تتيح لك هذه الأداة الانتقال من التشغيل عن بُعد إلى مكبِّر صوت الهاتف أو جهاز البلوتوث المحلي. ويمكن تفعيل ذلك من خلال ضبط
علامة setRemoteToLocalEnabled
على true
في CastOptions
.
في الحالات التي ينضم فيها جهاز المُرسِل الحالي إلى جلسة حالية مع عدة مُرسِلين ويحتاج التطبيق إلى التحقُّق ممّا إذا كان يُسمح بنقل الوسائط الحالية على الجهاز، يجب أن تستخدم التطبيقات معاودة الاتصال onTransferred
بالرمز SessionTransferCallback
للتحقّق من SessionState
.
ضبط العلامة setRemoteToLocalEnabled
توفّر "CastOptions
" setRemoteToLocalEnabled
لإظهار أو إخفاء
مكبّر صوت الهاتف وأجهزة البلوتوث المحلية كأهداف للنقل في مربّع حوار "أداة تبديل الإخراج" عندما تكون هناك جلسة بث نشِطة.
class CastOptionsProvider : OptionsProvider { fun getCastOptions(context: Context?): CastOptions { ... return Builder() ... .setRemoteToLocalEnabled(true) .build() } }
public class CastOptionsProvider implements OptionsProvider { @Override public CastOptions getCastOptions(Context context) { ... return new CastOptions.Builder() ... .setRemoteToLocalEnabled(true) .build() } }
متابعة التشغيل محليًا
على التطبيقات التي تتيح البث عن بُعد تسجيل جهاز SessionTransferCallback
لتلقّي إشعار عند وقوع الحدث حتى تتمكّن من التحقّق ممّا إذا كان يجب السماح بنقل الوسائط
ومواصلة تشغيلها على الجهاز.
تسمح CastContext#addSessionTransferCallback(SessionTransferCallback)
للتطبيق
بتسجيل SessionTransferCallback
الخاصة به والاستماع إلى استدعاءات onTransferred
وonTransferFailed
عندما يتم نقل مُرسِل إلى وضع التشغيل المحلي.
بعد أن يلغي التطبيق تسجيل SessionTransferCallback
، لن يتلقّى التطبيق
بعد ذلك SessionTransferCallback
.
SessionTransferCallback
هي إضافة لعمليات استدعاء
SessionManagerListener
الحالية ويتم تشغيلها بعد تشغيل onSessionEnded
. وبالتالي، يكون ترتيب استدعاءات
الاتصالات المحلية عن بعد على النحو التالي:
onTransferring
onSessionEnding
onSessionEnded
onTransferred
وبما أنّه يمكن فتح "أداة التبديل بين أجهزة التشغيل" بواسطة شريحة إشعارات الوسائط عندما يكون التطبيق قيد التشغيل في الخلفية أثناء البث، تحتاج التطبيقات إلى معالجة عملية النقل إلى الإعدادات المحلية بشكل مختلف حسب ما إذا كانت تتيح التشغيل في الخلفية أم لا. في حال تعذُّر عملية النقل، سيتم تنشيط onTransferFailed
في أي وقت يحدث فيه الخطأ.
التطبيقات التي تتيح التشغيل في الخلفية
بالنسبة إلى التطبيقات التي تتيح التشغيل في الخلفية (التطبيقات الصوتية عادةً)، يُنصح باستخدام Service
(مثل MediaBrowserService
). ومن المفترض أن تستمع الخدمات
إلى معاودة الاتصال عبر onTransferred
وتستأنف التشغيل على الجهاز عندما يكون التطبيق في المقدّمة أو في الخلفية.
class MyService : Service() { private var castContext: CastContext? = null private var sessionTransferCallback: SessionTransferCallback? = null protected fun onCreate() { castContext = CastContext.getSharedInstance(this) castContext.getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession::class.java) sessionTransferCallback = MySessionTransferCallback() castContext.addSessionTransferCallback(sessionTransferCallback) } protected fun onDestroy() { if (castContext != null) { castContext.getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession::class.java) if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback) } } } class MySessionTransferCallback : SessionTransferCallback() { fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) { // Perform necessary steps prior to onTransferred } fun onTransferred(@SessionTransferCallback.TransferType transferType: Int, sessionState: SessionState?) { if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int, @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) { // Handle transfer failure. } } }
public class MyService extends Service { private CastContext castContext; private SessionTransferCallback sessionTransferCallback; @Override protected void onCreate() { castContext = CastContext.getSharedInstance(this); castContext.getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession.class); sessionTransferCallback = new MySessionTransferCallback(); castContext.addSessionTransferCallback(sessionTransferCallback); } @Override protected void onDestroy() { if (castContext != null) { castContext.getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession.class); if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback); } } } public static class MySessionTransferCallback extends SessionTransferCallback { public MySessionTransferCallback() {} @Override public void onTransferring(@SessionTransferCallback.TransferType int transferType) { // Perform necessary steps prior to onTransferred } @Override public void onTransferred(@SessionTransferCallback.TransferType int transferType, SessionState sessionState) { if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } @Override public void onTransferFailed(@SessionTransferCallback.TransferType int transferType, @SessionTransferCallback.TransferFailedReason int transferFailedReason) { // Handle transfer failure. } } }
التطبيقات التي لا تتيح التشغيل في الخلفية
بالنسبة إلى التطبيقات التي لا تتيح التشغيل في الخلفية (تطبيقات الفيديو عادةً)، يُنصح بالاستماع إلى معاودة الاتصال بـ onTransferred
واستئناف التشغيل محليًا إذا كان التطبيق يعمل في المقدّمة.
إذا كان التطبيق قيد التشغيل في الخلفية، من المفترض أن يوقف التشغيل مؤقتًا ويخزّن
المعلومات اللازمة من SessionState
(مثل البيانات الوصفية للوسائط وموضع
التشغيل). عند تشغيل التطبيق في المقدّمة من الخلفية، يجب أن يستمر التشغيل المحلي
مع المعلومات المخزَّنة.
class MyActivity : AppCompatActivity() { private var castContext: CastContext? = null private var sessionTransferCallback: SessionTransferCallback? = null protected fun onCreate() { castContext = CastContext.getSharedInstance(this) castContext.getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession::class.java) sessionTransferCallback = MySessionTransferCallback() castContext.addSessionTransferCallback(sessionTransferCallback) } protected fun onDestroy() { if (castContext != null) { castContext.getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession::class.java) if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback) } } } class MySessionTransferCallback : SessionTransferCallback() { fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) { // Perform necessary steps prior to onTransferred } fun onTransferred(@SessionTransferCallback.TransferType transferType: Int, sessionState: SessionState?) { if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int, @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) { // Handle transfer failure. } } }
public class MyActivity extends AppCompatActivity { private CastContext castContext; private SessionTransferCallback sessionTransferCallback; @Override protected void onCreate() { castContext = CastContext.getSharedInstance(this); castContext .getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession.class); sessionTransferCallback = new MySessionTransferCallback(); castContext.addSessionTransferCallback(sessionTransferCallback); } @Override protected void onDestroy() { if (castContext != null) { castContext .getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession.class); if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback); } } } public static class MySessionTransferCallback extends SessionTransferCallback { public MySessionTransferCallback() {} @Override public void onTransferring(@SessionTransferCallback.TransferType int transferType) { // Perform necessary steps prior to onTransferred } @Override public void onTransferred(@SessionTransferCallback.TransferType int transferType, SessionState sessionState) { if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } @Override public void onTransferFailed(@SessionTransferCallback.TransferType int transferType, @SessionTransferCallback.TransferFailedReason int transferFailedReason) { // Handle transfer failure. } } }