4343import android .net .wifi .WifiManager ;
4444import android .os .Build ;
4545import android .os .Environment ;
46+ import android .os .Handler ;
47+ import android .os .Looper ;
4648import android .text .Html ;
4749import android .text .SpannableString ;
4850import android .text .method .LinkMovementMethod ;
@@ -118,6 +120,12 @@ public final class Util {
118120 private static AudioFocusRequest audioFocusRequest ;
119121 private static boolean pauseFocus = false ;
120122 private static boolean lowerFocus = false ;
123+ private static Handler mainHandler ;
124+ private static Runnable volumeFadeStep ;
125+ private static final float DUCK_VOLUME = 0.1f ;
126+ private static final int FADE_OUT_DURATION_MS = 250 ;
127+ private static final int FADE_IN_DURATION_MS = 400 ;
128+ private static final int FADE_STEP_MS = 16 ;
121129
122130 // Used by hexEncode()
123131 private static final char [] HEX_DIGITS = {'0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' };
@@ -1329,10 +1337,18 @@ public static void requestAudioFocus(final Context context, final AudioManager a
13291337 .setOnAudioFocusChangeListener (getAudioFocusChangeListener (context , audioManager ))
13301338 .setWillPauseWhenDucked (true )
13311339 .build ();
1332- audioManager .requestAudioFocus (audioFocusRequest );
1340+ int result = audioManager .requestAudioFocus (audioFocusRequest );
1341+ if (result != AudioManager .AUDIOFOCUS_REQUEST_GRANTED ) {
1342+ Log .w (TAG , "Audio focus request not granted: " + result );
1343+ audioFocusRequest = null ;
1344+ }
13331345 }
13341346 } else if (Build .VERSION .SDK_INT >= 8 && focusListener == null ) {
1335- audioManager .requestAudioFocus (focusListener = getAudioFocusChangeListener (context , audioManager ), AudioManager .STREAM_MUSIC , AudioManager .AUDIOFOCUS_GAIN );
1347+ int result = audioManager .requestAudioFocus (focusListener = getAudioFocusChangeListener (context , audioManager ), AudioManager .STREAM_MUSIC , AudioManager .AUDIOFOCUS_GAIN );
1348+ if (result != AudioManager .AUDIOFOCUS_REQUEST_GRANTED ) {
1349+ Log .w (TAG , "Audio focus request not granted: " + result );
1350+ focusListener = null ;
1351+ }
13361352 }
13371353 }
13381354
@@ -1345,10 +1361,17 @@ public void onAudioFocusChange(int focusChange) {
13451361 Log .i (TAG , "Temporary loss of focus" );
13461362 SharedPreferences prefs = getPreferences (context );
13471363 int lossPref = Integer .parseInt (prefs .getString (Constants .PREFERENCES_KEY_TEMP_LOSS , "1" ));
1348- if (lossPref == 2 || ( lossPref == 1 && focusChange == AudioManager . AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK ) ) {
1364+ if (lossPref == 2 ) {
13491365 lowerFocus = true ;
1350- downloadService .setVolume (0.1f );
1351- } else if (lossPref == 0 || (lossPref == 1 && focusChange == AudioManager .AUDIOFOCUS_LOSS_TRANSIENT )) {
1366+ fadeVolume (downloadService , DUCK_VOLUME , FADE_OUT_DURATION_MS , null );
1367+ } else if (lossPref == 1 ) {
1368+ // Smooth ducking for both CAN_DUCK (to 10%) and TRANSIENT (to 0%).
1369+ // Avoids the pause/resume round-trip whose race left audio silently
1370+ // playing after GAIN. Track stays running so GAIN can just fade back in.
1371+ lowerFocus = true ;
1372+ float target = (focusChange == AudioManager .AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK ) ? DUCK_VOLUME : 0f ;
1373+ fadeVolume (downloadService , target , FADE_OUT_DURATION_MS , null );
1374+ } else if (lossPref == 0 ) {
13521375 pauseFocus = true ;
13531376 downloadService .pause (true );
13541377 }
@@ -1360,7 +1383,7 @@ public void onAudioFocusChange(int focusChange) {
13601383 }
13611384 if (lowerFocus ) {
13621385 lowerFocus = false ;
1363- downloadService . setVolume ( 1.0f );
1386+ fadeVolume ( downloadService , 1.0f , FADE_IN_DURATION_MS , null );
13641387 }
13651388 } else if (focusChange == AudioManager .AUDIOFOCUS_LOSS && !downloadService .isRemoteEnabled ()) {
13661389 Log .i (TAG , "Permanently lost focus" );
@@ -1380,6 +1403,39 @@ public void onAudioFocusChange(int focusChange) {
13801403 };
13811404 }
13821405
1406+ private static void fadeVolume (final DownloadService downloadService , final float target , final int durationMs , final Runnable onComplete ) {
1407+ if (mainHandler == null ) {
1408+ mainHandler = new Handler (Looper .getMainLooper ());
1409+ }
1410+ mainHandler .post (new Runnable () {
1411+ @ Override
1412+ public void run () {
1413+ // Cancel any in-flight fade so back-to-back focus events don't stack up.
1414+ if (volumeFadeStep != null ) {
1415+ mainHandler .removeCallbacks (volumeFadeStep );
1416+ volumeFadeStep = null ;
1417+ }
1418+ final float start = downloadService .getVolume ();
1419+ final long startTime = android .os .SystemClock .uptimeMillis ();
1420+ volumeFadeStep = new Runnable () {
1421+ @ Override
1422+ public void run () {
1423+ long elapsed = android .os .SystemClock .uptimeMillis () - startTime ;
1424+ float t = Math .min (1f , durationMs > 0 ? (float ) elapsed / durationMs : 1f );
1425+ downloadService .setVolume (start + (target - start ) * t );
1426+ if (t < 1f ) {
1427+ mainHandler .postDelayed (this , FADE_STEP_MS );
1428+ } else {
1429+ volumeFadeStep = null ;
1430+ if (onComplete != null ) onComplete .run ();
1431+ }
1432+ }
1433+ };
1434+ volumeFadeStep .run ();
1435+ }
1436+ });
1437+ }
1438+
13831439 public static void abandonAudioFocus (Context context ) {
13841440 if (focusListener != null ) {
13851441 final AudioManager audioManager = (AudioManager ) context .getSystemService (Context .AUDIO_SERVICE );
0 commit comments