Skip to content

Commit

Permalink
Refactor CastAudioManager#requestAudioFocusWhen().
Browse files Browse the repository at this point in the history
This resolves a couple of TODOs in CastAudioManager and provides
a way to request audio focus AND listen to the actual audio
focus state reactively.

Merge-With: eureka-internal/193815

Bug: 112064938 (followup)
Test: cast_shell_junit_tests
Change-Id: I607995834221391d9f7c8b7b8944bc38dd1a03a1
Reviewed-on: https://chromium-review.googlesource.com/1176783
Commit-Queue: Simeon Anfinrud <sanfin@chromium.org>
Reviewed-by: Luke Halliwell <halliwell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#588549}
  • Loading branch information
Simeon Anfinrud authored and Commit Bot committed Sep 4, 2018
1 parent 80b41d9 commit 22c8b9e
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@
import android.content.Context;
import android.media.AudioManager;
import android.os.Build;
import android.support.annotation.Nullable;

import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.chromecast.base.Controller;
import org.chromium.chromecast.base.Observable;
import org.chromium.chromecast.base.Unit;

/**
* Wrapper for Cast code to use a single AudioManager instance.
* Muting and unmuting streams must be invoke on the same AudioManager instance.
*
* Encapsulates behavior that differs across SDK levels like muting and audio focus, and manages a
* singleton instance that ensures that all clients are using the same AudioManager.
*/
public class CastAudioManager {
private static final String TAG = "CastAudioManager";
// TODO(sanfin): This class should encapsulate SDK-dependent implementation details of
// android.media.AudioManager.
private static CastAudioManager sInstance = null;

public static CastAudioManager getAudioManager(Context context) {
Expand All @@ -32,50 +32,55 @@ public static CastAudioManager getAudioManager(Context context) {
return sInstance;
}

private final AudioManager mAudioManager;
private final AudioManager mInternal;

@VisibleForTesting
CastAudioManager(AudioManager audioManager) {
mAudioManager = audioManager;
mInternal = audioManager;
}

/**
* Requests audio focus whenever the given Observable is activated.
*
* Returns an Observable that is activated whenever the audio focus is granted.
* The audio focus request is abandoned when the given Observable is deactivated.
*
* TODO(sanfin): Distinguish between transient, ducking, and full audio focus losses.
* Returns an Observable that is activated whenever the audio focus is lost. The activation data
* of this Observable indicates the type of audio focus loss.
*
* The resulting Observable will be activated with AudioFocus.NORMAL when the focus request is
* abandoned.
*
* Observable<AudioFocusLoss> focusLoss = castAudioManager.requestFocusWhen(focusRequest);
* // Get an Observable of when focus is taken:
* Observable<Unit> gotFocus = Observable.not(focusLoss);
* // Get an Observable of when a specific request got focus:
* Observable<Both<CastAudioFocusRequest, AudioFocusLoss>> requestLost =
* focusRequest.andThen(focusLoss);
*
* The given Observable<CastAudioFocusRequest> should deactivate before it is garbage-collected,
* or else the Observable and anything it references will leak.
*/
public Observable<Unit> requestAudioFocusWhen(
Observable<?> event, CastAudioFocusRequest castAudioFocusRequest) {
Controller<Unit> audioFocusState = new Controller<>();
event.subscribe(x -> {
AudioManager.OnAudioFocusChangeListener listener = (int focusChange) -> {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
audioFocusState.set(Unit.unit());
return;
default:
audioFocusState.reset();
return;
}
};
castAudioFocusRequest.setAudioFocusChangeListener(listener);
public Observable<AudioFocusLoss> requestAudioFocusWhen(
Observable<CastAudioFocusRequest> event) {
Controller<AudioFocusLoss> audioFocusLossState = new Controller<>();
audioFocusLossState.set(AudioFocusLoss.NORMAL);
event.subscribe(focusRequest -> {
focusRequest.setAudioFocusChangeListener((int focusChange) -> {
audioFocusLossState.set(AudioFocusLoss.from(focusChange));
});
// Request audio focus when the source event is activated.
if (requestAudioFocus(castAudioFocusRequest)
!= AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.e(TAG, "Failed to get audio focus");
if (focusRequest.request(mInternal) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
audioFocusLossState.reset();
}
// Abandon audio focus when the source event is deactivated.
return () -> {
if (abandonAudioFocus(castAudioFocusRequest)
!= AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
if (focusRequest.abandon(mInternal) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.e(TAG, "Failed to abandon audio focus");
}
audioFocusState.reset();
audioFocusLossState.set(AudioFocusLoss.NORMAL);
};
});
return audioFocusState;
return audioFocusLossState;
}

// Only called on Lollipop and below, in an Activity's onPause() event.
Expand All @@ -93,35 +98,50 @@ public void releaseStreamMuteIfNecessary(int streamType) {
try {
// isStreamMute() was only made public in M, but it can be accessed through
// reflection in L.
isMuted = (Boolean) mAudioManager.getClass()
isMuted = (Boolean) mInternal.getClass()
.getMethod("isStreamMute", int.class)
.invoke(mAudioManager, streamType);
.invoke(mInternal, streamType);
} catch (Exception e) {
Log.e(TAG, "Can not call AudioManager.isStreamMute().", e);
}

if (isMuted) {
// Note: this is a no-op on fixed-volume devices.
mAudioManager.setStreamMute(streamType, false);
mInternal.setStreamMute(streamType, false);
}
}
}

public int requestAudioFocus(CastAudioFocusRequest castAudioFocusRequest) {
return castAudioFocusRequest.request(mAudioManager);
}

public int abandonAudioFocus(CastAudioFocusRequest castAudioFocusRequest) {
return castAudioFocusRequest.abandon(mAudioManager);
}

public int getStreamMaxVolume(int streamType) {
return mAudioManager.getStreamMaxVolume(streamType);
return mInternal.getStreamMaxVolume(streamType);
}

// TODO(sanfin): Do not expose this. All needed AudioManager methods can be adapted with
// CastAudioManager.
public AudioManager getInternal() {
return mAudioManager;
return mInternal;
}

/**
* Disambiguates different audio focus loss types that can activate the result of
* requestAudioFocusWhen().
*/
public enum AudioFocusLoss {
NORMAL,
TRANSIENT,
TRANSIENT_CAN_DUCK;

private static @Nullable AudioFocusLoss from(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
return NORMAL;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
return TRANSIENT;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
return TRANSIENT_CAN_DUCK;
default:
return null;
}
}
}
}
Loading

0 comments on commit 22c8b9e

Please sign in to comment.