ניהול המיקוד באודיו

שתי אפליקציות Android או יותר יכולות להפעיל אודיו באותו מקור פלט בו-זמנית, והמערכת מערבבת את כולן. זהו יכול להיות מרשים מבחינה טכנית, אבל יכול להיות מאוד מתסכל למשתמש. כדי למנוע מצב שבו כל אפליקציית מוזיקה תופעל בו-זמנית, ב-Android הוספנו את התכונה מיקוד אודיו. רק אפליקציה אחת יכולה להתמקד באודיו בכל פעם.

כשהאפליקציה צריכה להפיק אודיו, היא צריכה לבקש הרשאת שליטה באודיו. כשהיא בחזית, היא יכולה להשמיע צלילים. עם זאת, יכול להיות שאי אפשר יהיה לשמור על המיקוד באודיו עד לסיום ההפעלה. אפליקציה אחרת יכולה לבקש להתמקד בה, וכך לבטל את ההחזקה שלכם על התמקדות האודיו. במקרה כזה, האפליקציה צריכה להשהות את ההפעלה או להנמיך את עוצמת הקול כדי לאפשר למשתמשים לשמוע בקלות רבה יותר את מקור האודיו החדש.

לפני Android 12 (רמת API ‏31), המערכת לא מנהלת את המיקוד האודיו. לכן, למרות שמפתחי אפליקציות מומלצים לפעול בהתאם להנחיות בנושא התמקדות באודיו, אם אפליקציה ממשיכה להשמיע קול רם גם אחרי שהיא מפסיקה להיות מוגדרת כממוקדת באודיו במכשיר עם Android 11 (רמת API ‏30) או גרסה ישנה יותר, המערכת לא יכולה למנוע זאת. עם זאת, התנהגות האפליקציה הזו מובילה לחוויית משתמש גרועה, ולעיתים קרובות המשתמשים מסירים את האפליקציה שמתנהגת בצורה לא תקינה.

אפליקציית אודיו שתוכננה היטב צריכה לנהל את מיקוד האודיו בהתאם להנחיות הכלליות הבאות:

  • מתקשרים למספר requestAudioFocus() מיד לפני שמתחילים לנגן ומאמתים שהשיחה מחזירה את הערך AUDIOFOCUS_REQUEST_GRANTED. מבצעים את השיחה אל requestAudioFocus() בקריאה החוזרת onPlay() של סשן המדיה.

  • כשאפליקציה אחרת מקבלת את המיקוד של האודיו, אפשר להפסיק או להשהות את ההפעלה או להקטין את עוצמת הקול.

  • כשההפעלה נעצרת (לדוגמה, כשאין יותר תוכן באפליקציה שאפשר להפעיל), צריך לבטל את המיקוד באודיו. האפליקציה לא צריכה לוותר על התמקדות באודיו אם המשתמש משהה את ההפעלה, אבל יכול להיות שהוא ימשיך את ההפעלה מאוחר יותר.

  • משתמשים ב-AudioAttributes כדי לתאר את סוג האודיו שהאפליקציה מפעילה. לדוגמה, באפליקציות שמשמיעות דיבור, צריך לציין את הערך CONTENT_TYPE_SPEECH.

המערכת מטפלת במיקוד האודיו באופן שונה בהתאם לגרסה של Android שפועלת במכשיר:

Android 12 (רמת API ‏31) ואילך
המערכת מנהלת את המיקוד באודיו. המערכת מאלצת את הפעלת האודיו מאפליקציה מסוימת להתפוגג כשאפליקציה אחרת מבקשת להתמקד באודיו. בנוסף, כשמקבלים שיחה נכנסת, המערכת משתיקה את הפעלת האודיו.
Android 8.0 (רמת API 26) עד Android 11 (רמת API 30)
המערכת לא מנהלת את התמקדות האודיו, אבל היא כוללת כמה שינויים שהושקו החל מגרסה 8.0 של Android (רמת API‏ 26).
Android 7.1 (רמת API 25) ומטה
המערכת לא מנהלת את התמקדות האודיו, והאפליקציות מנהלות את התמקדות האודיו באמצעות הלחצנים requestAudioFocus() ו-abandonAudioFocus().

מיקוד אודיו ב-Android מגרסה 12 ואילך

אפליקציית מדיה או משחק שמשתמשת בהתמקדות באודיו לא אמורה להפעיל אודיו אחרי שהיא מאבדת את ההתמקדות. ב-Android 12 (רמת API 31) ואילך, המערכת אוכפת את ההתנהגות הזו. כשאפליקציה מבקשת להתמקד באודיו בזמן שאפליקציה אחרת ממוקדת ומשמיעה אודיו, המערכת מאלצת את האפליקציה שמשמיעה אודיו לדעוך. הוספת ההעלמה מאפשרת מעבר חלק יותר בין אפליקציות.

ההתנהגות הזו מתרחשת כשמתקיימים התנאים הבאים:

  1. האפליקציה הראשונה שפועלת כרגע עומדת בכל הקריטריונים הבאים:

  2. אפליקציה שנייה מבקשת הרשאת אודיו באמצעות AudioManager.AUDIOFOCUS_GAIN.

כשהתנאים האלה מתקיימים, מערכת האודיו מפחיתה בהדרגה את עוצמת הקול באפליקציה הראשונה. בסיום ההפחתה, המערכת מעדכנת את האפליקציה הראשונה על אובדן המיקוד. נגני האפליקציה יישארו מושתקים עד שהאפליקציה תבקש שוב את הרשאת האודיו.

התנהגויות קיימות של מיקוד אודיו

חשוב גם לדעת על המקרים הבאים שבהם מתבצע מעבר של מוקד האודיו.

הנמכה אוטומטית של עוצמת הקול

התכונה 'הנמכה אוטומטית של עוצמת השמע' (הפחתה זמנית של עוצמת השמע באפליקציה אחת כדי שאפשר יהיה לשמוע אפליקציה אחרת בבירור) הושקה ב-Android 8.0 (רמת API‏ 26).

כשהמערכת מטמיעה את ההשתקה, אין צורך להטמיע אותה באפליקציה.

השקטה אוטומטית מתרחשת גם כשהתמקדות עוברת מאפליקציה שפועלת להפעלת אודיו להתראה קולית. תחילת ההפעלה של ההתראה מסונכרנת עם סיום הירידה בעצמת הקול.

ההשתקה האוטומטית מתרחשת כשהתנאים הבאים מתקיימים:

  1. האפליקציה הראשונה שמופעלת כרגע עומדת בכל הקריטריונים הבאים:

  2. אפליקציה שנייה מבקשת הרשאת אודיו באמצעות AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK.

כשהתנאים האלה מתקיימים, מערכת האודיו משתיקה את כל הנגנים הפעילים באפליקציה הראשונה בזמן שהאפליקציה השנייה נמצאת בחזית. כשהאפליקציה השנייה מפסיקה להיות במוקד, התמונות והסרטונים חוזרים למצב רגיל. האפליקציה הראשונה לא מקבלת התראה כשהיא מאבדת את המיקוד, ולכן היא לא צריכה לעשות כלום.

חשוב לזכור שהשתקה אוטומטית לא מתבצעת כשהמשתמש מקשיב לתוכן דיבור, כי יכול להיות שהוא יחמיץ חלק מהתוכנית. לדוגמה, הנחיות קוליות למסלולי נסיעה לא מושתקות.

השתקת ההפעלה הנוכחית של האודיו בשיחות טלפון נכנסות

חלק מהאפליקציות לא פועלות כראוי וממשיכות להפעיל אודיו במהלך שיחות טלפון. המצב הזה מאלץ את המשתמש למצוא את האפליקציה הבעייתית ולהשתיק אותה או לצאת ממנה כדי לשמוע את השיחה. כדי למנוע זאת, המערכת יכולה להשתיק את האודיו מאפליקציות אחרות בזמן שיחה נכנסת. המערכת מפעילה את התכונה הזו כשמתקבלת שיחה נכנסת בטלפון ואפליקציה עומדת בתנאים הבאים:

  • לאפליקציה יש את מאפיין השימוש AudioAttributes.USAGE_MEDIA או AudioAttributes.USAGE_GAME.
  • האפליקציה ביקשה להתמקד באודיו (כל התמקדות) והיא מפעילה אודיו.

אם אפליקציה ממשיכה לפעול במהלך השיחה, ההפעלה שלה תושתק עד לסיום השיחה. עם זאת, אם אפליקציה מתחילה לפעול במהלך השיחה, הנגן לא מושתק מתוך הנחה שהמשתמש הפעיל את ההפעלה בכוונה.

התמקדות באודיו ב-Android מגרסה 8.0 ועד Android 11

החל מגרסה Android 8.0‏ (רמת API‏ 26), כשקוראים ל-requestAudioFocus() צריך לספק פרמטר AudioFocusRequest. השדה AudioFocusRequest מכיל מידע על ההקשר והיכולות של השמע באפליקציה. המערכת משתמשת במידע הזה כדי לנהל באופן אוטומטי את העלייה והירידה ברמת המיקוד של השמע. כדי לשחרר את הרשאת האודיו, צריך לבצע קריאה ל-method‏ abandonAudioFocusRequest(), שגם הוא מקבל את AudioFocusRequest כארגומנטים. צריך להשתמש באותו מופע של AudioFocusRequest גם כשמבקשים להתמקד וגם כשמבטלים את ההתמקדות.

כדי ליצור AudioFocusRequest, משתמשים ב-AudioFocusRequest.Builder. מכיוון שבבקשת התמקדות תמיד צריך לציין את סוג הבקשה, הסוג נכלל ב-constructor של ה-builder. משתמשים בשיטות של ה-builder כדי להגדיר את שאר השדות של הבקשה.

השדה FocusGain הוא חובה, כל שאר השדות הם אופציונליים.

שיטההערות
setFocusGain() השדה הזה נדרש בכל בקשה. הוא מקבל את אותם ערכים כמו durationHint ששימש בקריאה ל-requestAudioFocus() לפני Android 8.0: AUDIOFOCUS_GAIN,‏ AUDIOFOCUS_GAIN_TRANSIENT,‏ AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK או AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE.
setAudioAttributes() AudioAttributes מתאר את התרחיש לדוגמה של האפליקציה. המערכת בודקת את התרחישים האלה כשאפליקציה מקבלת או מאבדת את המיקוד של האודיו. המאפיינים מחליפים את הרעיון של סוג הסטרימינג. ב-Android מגרסה 8.0 (רמת API‏ 26) ואילך, סוגי הסטרימינג של כל פעולה מלבד אמצעי הבקרה של עוצמת הקול הוצאו משימוש. צריך להשתמש באותם מאפיינים בבקשת המיקוד שבה אתם משתמשים בנגן האודיו (כפי שמוצג בדוגמה שמופיעה אחרי הטבלה הזו).

קודם צריך להשתמש ב-AudioAttributes.Builder כדי לציין את המאפיינים, ואז להשתמש בשיטה הזו כדי להקצות את המאפיינים לבקשה.

אם לא מציינים ערך, AudioAttributes מוגדר כברירת מחדל כ-AudioAttributes.USAGE_MEDIA.

setWillPauseWhenDucked() כשאפליקציה אחרת מבקשת להתמקד באמצעות AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, בדרך כלל לא מתקבל קריאה חוזרת (callback) של onAudioFocusChange() באפליקציה שבה יש מיקוד, כי המערכת יכולה לבצע את ההסתרה בעצמה. אם אתם צריכים להשהות את ההפעלה במקום להנמיך את עוצמת הקול, צריך להפעיל את הפונקציה setWillPauseWhenDucked(true) וליצור ולהגדיר אירוע OnAudioFocusChangeListener, כפי שמתואר בקטע הנמכת עוצמת קול אוטומטית.
setAcceptsDelayedFocusGain() בקשה להרשאת אודיו עשויה להיכשל אם אפליקציה אחרת נעולה עליה. השיטה הזו מאפשרת קבלת הרשאת אודיו באיחור: היכולת לקבל את ההרשאה באופן אסינכרוני כשהיא זמינה.

חשוב לזכור שהפעלת הבקשה להעברת המיקוד באיחור פועלת רק אם מציינים גם את הערך AudioManager.OnAudioFocusChangeListener בבקשת האודיו, כי האפליקציה צריכה לקבל את הקריאה החוזרת כדי לדעת שהבקשה להעברת המיקוד אושרה.

setOnAudioFocusChangeListener() צריך לציין OnAudioFocusChangeListener רק אם מציינים גם את willPauseWhenDucked(true) או את setAcceptsDelayedFocusGain(true) בבקשה.

יש שתי שיטות להגדרת המאזין: אחת עם ארגומנטים של טיפול והשנייה בלי. הטיפול הוא השרשור שבו פועל המאזין. אם לא מציינים טיפול, המערכת משתמשת בטיפול שמשויך ל-Looper הראשי.

בדוגמה הבאה מוסבר איך להשתמש ב-AudioFocusRequest.Builder כדי ליצור AudioFocusRequest, לבקש את המיקוד באודיו ולבטל אותו:

Kotlin

// initializing variables for audio focus and playback management
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
    setAudioAttributes(AudioAttributes.Builder().run {
        setUsage(AudioAttributes.USAGE_GAME)
        setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        build()
    })
    setAcceptsDelayedFocusGain(true)
    setOnAudioFocusChangeListener(afChangeListener, handler)
    build()
}
val focusLock = Any()

var playbackDelayed = false
var playbackNowAuthorized = false

// requesting audio focus and processing the response
val res = audioManager.requestAudioFocus(focusRequest)
synchronized(focusLock) {
    playbackNowAuthorized = when (res) {
        AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
        AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
            playbackNow()
            true
        }
        AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
            playbackDelayed = true
            false
        }
        else -> false
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
override fun onAudioFocusChange(focusChange: Int) {
    when (focusChange) {
        AudioManager.AUDIOFOCUS_GAIN ->
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false
                    resumeOnFocusGain = false
                }
                playbackNow()
            }
        AudioManager.AUDIOFOCUS_LOSS -> {
            synchronized(focusLock) {
                resumeOnFocusGain = false
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying()
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // ... pausing or ducking depends on your app
        }
    }
}

Java

// initializing variables for audio focus and playback management
audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
playbackAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_GAME)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();
focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(playbackAttributes)
        .setAcceptsDelayedFocusGain(true)
        .setOnAudioFocusChangeListener(afChangeListener, handler)
        .build();
final Object focusLock = new Object();

boolean playbackDelayed = false;
boolean playbackNowAuthorized = false;

// requesting audio focus and processing the response
int res = audioManager.requestAudioFocus(focusRequest);
synchronized(focusLock) {
    if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
        playbackNowAuthorized = false;
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        playbackNowAuthorized = true;
        playbackNow();
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
        playbackDelayed = true;
        playbackNowAuthorized = false;
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
@Override
public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false;
                    resumeOnFocusGain = false;
                }
                playbackNow();
            }
            break;
        case AudioManager.AUDIOFOCUS_LOSS:
            synchronized(focusLock) {
                resumeOnFocusGain = false;
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying();
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // ... pausing or ducking depends on your app
            break;
        }
    }
}

הנמכה אוטומטית של עוצמת הקול

ב-Android 8.0 (רמת API‏ 26), כשאפליקציה אחרת מבקשת להתמקד באמצעות AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, המערכת יכולה להפחית את עוצמת הקול ולשחזר אותה בלי להפעיל את פונקציית ה-callback‏ onAudioFocusChange() של האפליקציה.

אמנם השהיה אוטומטית היא התנהגות מקובלת באפליקציות להפעלת מוזיקה וסרטונים, אבל היא לא שימושית כשמפעילים תוכן דיבור, למשל באפליקציית אודיו-ספרים. במקרה כזה, האפליקציה צריכה להשהות את התוכן במקום זאת.

אם אתם רוצים שהאפליקציה תיפסק כשמתבקשים להשתיק אותה במקום להפחית את עוצמת הקול, צריך ליצור OnAudioFocusChangeListener עם שיטה של onAudioFocusChange() חזרה (callback) שמטמיעה את ההתנהגות הרצויה של השהיה/המשך. קוראים ל-setOnAudioFocusChangeListener() כדי לרשום את המאזין, וקוראים ל-setWillPauseWhenDucked(true) כדי להורות למערכת להשתמש בקריאה החוזרת שלכם במקום לבצע הנמכה אוטומטית של עוצמת הקול.

התחזקות מטושטשת

לפעמים המערכת לא יכולה לאשר בקשה להתמקד באודיו כי האפליקציה אחרת 'נעל' את המיקוד, למשל במהלך שיחת טלפון. במקרה הזה, הפונקציה requestAudioFocus() מחזירה את הערך AUDIOFOCUS_REQUEST_FAILED. במקרה כזה, האפליקציה לא אמורה להמשיך בהפעלת האודיו כי היא לא קיבלה את המיקוד.

השיטה setAcceptsDelayedFocusGain(true) שמאפשרת לאפליקציה לטפל בבקשה להעברת המיקוד באופן אסינכרוני. כשהדגל הזה מוגדר, בקשה שנשלחת כשהמיקוד נעול מחזירה את הערך AUDIOFOCUS_REQUEST_DELAYED. כשהתנאי שגרם לנעילת המיקוד של האודיו לא קיים יותר, למשל כששיחת הטלפון מסתיימת, המערכת מעניקה את בקשת המיקוד בהמתנה ומפעילה את onAudioFocusChange() כדי להודיע לאפליקציה.

כדי לטפל בהשגת המיקוד המאוחר, צריך ליצור OnAudioFocusChangeListener עם method onAudioFocusChange() של קריאה חוזרת שמטמיע את ההתנהגות הרצויה, ולרשום את המאזין באמצעות קריאה ל-setOnAudioFocusChangeListener().

מיקוד אודיו ב-Android מגרסה 7.1 ומטה

כשקוראים ל-requestAudioFocus(), צריך לציין רמז למשך הזמן. אפליקציה אחרת שמתמקדת כרגע ומפעילה תוכן עשויה לפעול לפי הרמז:

  • כדאי לבקש שליטה קבועה באודיו (AUDIOFOCUS_GAIN) אם אתם מתכננים להפעיל אודיו בעתיד הקרוב (לדוגמה, כשאתם מנגנים מוזיקה) ואתם מצפים שהאפליקציה הקודמת שהחזיקה בשליטה באודיו תפסיק להפעיל אודיו.
  • כדאי לבקש מיקוד זמני (AUDIOFOCUS_GAIN_TRANSIENT) כשאתם מתכוונים להפעיל אודיו למשך זמן קצר בלבד, ומצפים שהבעלים הקודם יפסיק את ההפעלה.
  • אפשר לבקש להתמקד באופן זמני עם דעיכה (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) כדי לציין שאתם מתכוונים להפעיל אודיו רק לזמן קצר, ושזה בסדר אם הבעלים הקודם של המיקוד ימשיך להפעיל את התוכן שלו אם הוא 'ידועך' (ירידה) את פלט האודיו שלו. שני פלטי האודיו מעורבבים בשידור האודיו. הדחקה מתאימה במיוחד לאפליקציות שמשתמשות בסטרימינג של אודיו לסירוגין, למשל להנחיות קוליות לנהיגה.

השיטה requestAudioFocus() דורשת גם AudioManager.OnAudioFocusChangeListener. צריך ליצור את המאזין הזה באותה פעילות או באותו שירות שבבעלותם סשן המדיה. הוא מטמיע את פונקציית ה-callback onAudioFocusChange() שהאפליקציה מקבלת כשאפליקציה אחרת מקבלת או מפסיקה את המיקוד באודיו.

קטע הקוד הבא מבקש להתמקד באודיו באופן קבוע בסטרימינג STREAM_MUSIC ומירשם OnAudioFocusChangeListener כדי לטפל בשינויים הבאים בהתמקדות באודיו. (הסבר על מאזין השינויים מופיע בקטע תגובה לשינוי המיקוד של האודיו).

Kotlin

audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
lateinit var afChangeListener AudioManager.OnAudioFocusChangeListener

...
// Request audio focus for playback
val result: Int = audioManager.requestAudioFocus(
        afChangeListener,
        // Use the music stream.
        AudioManager.STREAM_MUSIC,
        // Request permanent focus.
        AudioManager.AUDIOFOCUS_GAIN
)

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

Java

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener afChangeListener;

...
// Request audio focus for playback
int result = audioManager.requestAudioFocus(afChangeListener,
                             // Use the music stream.
                             AudioManager.STREAM_MUSIC,
                             // Request permanent focus.
                             AudioManager.AUDIOFOCUS_GAIN);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

כשמסיימים את ההפעלה, מקישים על abandonAudioFocus().

Kotlin

audioManager.abandonAudioFocus(afChangeListener)

Java

// Abandon audio focus when playback complete
audioManager.abandonAudioFocus(afChangeListener);

הפעולה הזו תודיע למערכת שאתם לא זקוקים יותר ל-focus, ותבטל את הרישום של OnAudioFocusChangeListener המשויך. אם תבקשו להעביר את המיקוד באופן זמני, האפליקציה שתושהה או שתוצג בהשתקה תקבל הודעה על כך שאפשר להמשיך את ההפעלה או לשחזר את עוצמת הקול שלה.

תגובה לשינוי המיקוד של האודיו

כשאפליקציה מקבלת את המיקוד באודיו, היא צריכה להיות מסוגלת לשחרר אותו כשאפליקציה אחרת מבקשת לקבל את המיקוד באודיו. במקרה כזה, האפליקציה מקבלת קריאה ל-method‏ onAudioFocusChange() ב-AudioFocusChangeListener שציינתם כשהאפליקציה התקשרה ל-requestAudioFocus().

הפרמטר focusChange שמוענק ל-onAudioFocusChange() מציין את סוג השינוי שמתרחש. הוא תואם לאינדיקטור משך הזמן שבו משתמשת האפליקציה שמקבלת את המיקוד. האפליקציה אמורה להגיב בצורה הולמת.

אובדן מיקוד זמני
אם שינוי המיקוד הוא זמני (AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK או AUDIOFOCUS_LOSS_TRANSIENT), האפליקציה צריכה להשתיק את האודיו (אם אתם לא מסתמכים על השתקה אוטומטית) או להשהות את ההפעלה, אבל לשמור על אותו מצב בכל שאר הזמן.

במהלך אובדן זמני של הרשאת האודיו, כדאי להמשיך לעקוב אחרי השינויים בהרשאת האודיו ולהתכונן להמשיך את ההפעלה הרגילה כשהיא תוחזר. כשהאפליקציה החוסמת מפסיקה להתמקד, תקבלו קריאה חוזרת (AUDIOFOCUS_GAIN). בשלב הזה תוכלו להחזיר את עוצמת הקול לרמה הרגילה או להפעיל מחדש את ההפעלה.

אובדן פוקוס קבוע
אם אובדן המיקוד באודיו הוא קבוע (AUDIOFOCUS_LOSS), אפליקציה אחרת מפעילה אודיו. האפליקציה צריכה להשהות את ההפעלה באופן מיידי, כי היא לעולם לא תקבל קריאה חוזרת מ-AUDIOFOCUS_GAIN. כדי להפעיל מחדש את ההפעלה, המשתמש צריך לבצע פעולה מפורשת, כמו לחיצה על לחצן ההפעלה בהתראה או בממשק המשתמש של האפליקציה.

קטע הקוד הבא מראה איך מטמיעים את OnAudioFocusChangeListener ואת הפונקציה החוזרת (callback) שלו, onAudioFocusChange(). שימו לב לשימוש ב-Handler כדי לעכב את הקריאה החוזרת להפסקה במקרה של אובדן אודיו סופי.

Kotlin

private val handler = Handler()
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
    when (focusChange) {
        AudioManager.AUDIOFOCUS_LOSS -> {
            // Permanent loss of audio focus
            // Pause playback immediately
            mediaController.transportControls.pause()
            // Wait 30 seconds before stopping playback
            handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            // Pause playback
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // Lower the volume, keep playing
        }
        AudioManager.AUDIOFOCUS_GAIN -> {
            // Your app has been granted audio focus again
            // Raise volume to normal, restart playback if necessary
        }
    }
}

Java

private Handler handler = new Handler();
AudioManager.OnAudioFocusChangeListener afChangeListener =
  new AudioManager.OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
      if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
        // Permanent loss of audio focus
        // Pause playback immediately
        mediaController.getTransportControls().pause();
        // Wait 30 seconds before stopping playback
        handler.postDelayed(delayedStopRunnable,
          TimeUnit.SECONDS.toMillis(30));
      }
      else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
        // Pause playback
      } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
        // Lower the volume, keep playing
      } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
        // Your app has been granted audio focus again
        // Raise volume to normal, restart playback if necessary
      }
    }
  };

הטיפול משתמש ב-Runnable שנראה כך:

Kotlin

private var delayedStopRunnable = Runnable {
    mediaController.transportControls.stop()
}

Java

private Runnable delayedStopRunnable = new Runnable() {
    @Override
    public void run() {
        getMediaController().getTransportControls().stop();
    }
};

כדי לוודא שההפסקה המתוזמנת לא תופעל אם המשתמש יפעיל מחדש את ההפעלה, צריך להפעיל את mHandler.removeCallbacks(mDelayedStopRunnable) בתגובה לשינויים במצב. לדוגמה, צריך לקרוא ל-removeCallbacks() ב-onPlay(), ב-onSkipToNext() וכו' של פונקציית ה-Callback. צריך גם לקרוא ל-method הזה ב-Callback‏ onDestroy() של השירות כשמנקים את המשאבים שבהם השירות משתמש.