blob: 2b92a9ba6a963d27efa8454ff1a36d8b201bc649 [file] [log] [blame]
/**
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.radio.audio;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.session.PlaybackState;
import android.util.IndentingPrintWriter;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.radio.platform.RadioTunerExt;
import com.android.car.radio.platform.RadioTunerExt.TuneCallback;
import com.android.car.radio.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/**
* Manages radio's audio stream.
*/
public class AudioStreamController {
private static final String TAG = "BcRadioApp.audio";
/** Tune operation. */
public static final int OPERATION_TUNE = 1;
/** Seek forward operation. */
public static final int OPERATION_SEEK_FWD = 2;
/** Seek backwards operation. */
public static final int OPERATION_SEEK_BKW = 3;
/** Step forwards operation. */
public static final int OPERATION_STEP_FWD = 4;
/** Step backwards operation. */
public static final int OPERATION_STEP_BKW = 5;
/**
* Operation types for {@link #preparePlayback}.
*/
@IntDef(value = {
OPERATION_TUNE,
OPERATION_SEEK_FWD,
OPERATION_SEEK_BKW,
OPERATION_STEP_FWD,
OPERATION_STEP_BKW,
})
@Retention(RetentionPolicy.SOURCE)
public @interface PlaybackOperation {}
private final Object mLock = new Object();
private final AudioManager mAudioManager;
private final RadioTunerExt mRadioTunerExt;
private final PlaybackStateCallback mCallback;
private final AudioFocusRequest mGainFocusReq;
/**
* Indicates that the app has *some* focus or a promise of it.
*
* It may be ducked, transiently lost or delayed.
*/
private boolean mHasSomeFocus;
private int mCurrentPlaybackState = PlaybackState.STATE_NONE;
private Object mTuningToken;
/**
* Callback for playback state changes.
*/
public interface PlaybackStateCallback {
/**
* Called when playback state changes.
*/
void onPlaybackStateChanged(int newState);
}
/**
* New (and only) instance of Audio stream controller.
*
* This is a part of RadioAppService that handles audio streams and playback status.
*
* @param context Context
* @param radioManager tuner hardware manager
* @param callback Callback for playback state changes
*/
public AudioStreamController(@NonNull Context context, @NonNull RadioTunerExt tuner,
@NonNull PlaybackStateCallback callback) {
mAudioManager = Objects.requireNonNull(
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
mRadioTunerExt = Objects.requireNonNull(tuner);
mCallback = Objects.requireNonNull(callback);
AudioAttributes playbackAttr = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
mGainFocusReq = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttr)
.setAcceptsDelayedFocusGain(true)
.setWillPauseWhenDucked(true)
.setOnAudioFocusChangeListener(this::onAudioFocusChange)
.build();
}
private boolean unmuteLocked() {
if (mRadioTunerExt.setMuted(false)) return true;
Log.w(TAG, "Failed to unmute, dropping audio focus");
abandonAudioFocusLocked();
return false;
}
private boolean requestAudioFocusLocked() {
if (mHasSomeFocus) return true;
int res = mAudioManager.requestAudioFocus(mGainFocusReq);
if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
Log.d(TAG, "Audio focus request is delayed");
mHasSomeFocus = true;
return true;
}
if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.w(TAG, "Couldn't obtain audio focus, res=" + res);
return false;
}
Log.v(TAG, "Audio focus request succeeded");
mHasSomeFocus = true;
// we assume that audio focus was requested only when we mean to unmute
if (!unmuteLocked()) return false;
return true;
}
private boolean abandonAudioFocusLocked() {
if (!mHasSomeFocus) return true;
if (!mRadioTunerExt.setMuted(true)) return false;
int res = mAudioManager.abandonAudioFocusRequest(mGainFocusReq);
if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.e(TAG, "Couldn't abandon audio focus, res=" + res);
return false;
}
Log.v(TAG, "Audio focus abandoned");
mHasSomeFocus = false;
return true;
}
private void notifyPlaybackStateLocked(int newState) {
if (mCurrentPlaybackState == newState) return;
mCurrentPlaybackState = newState;
Log.v(TAG, "Playback state changed to " + newState);
mCallback.onPlaybackStateChanged(newState);
}
/**
* Prepare playback for ongoing tune/scan operation.
*
* @param operation Playback operation type
* @return result callback to be passed to {@link RadioTunerExt#tune} call,
* or {@code null} if couldn't get focus.
*/
@Nullable
public TuneCallback preparePlayback(@PlaybackOperation int operation) {
synchronized (mLock) {
Object token = new Object();
mTuningToken = token;
if (!requestAudioFocusLocked()) {
mTuningToken = null;
return null;
}
int state;
switch (operation) {
case OPERATION_TUNE:
state = PlaybackState.STATE_CONNECTING;
break;
case OPERATION_SEEK_FWD:
case OPERATION_STEP_FWD:
state = PlaybackState.STATE_SKIPPING_TO_NEXT;
break;
case OPERATION_SEEK_BKW:
case OPERATION_STEP_BKW:
state = PlaybackState.STATE_SKIPPING_TO_PREVIOUS;
break;
default:
throw new IllegalArgumentException("Invalid operation: " + operation);
}
notifyPlaybackStateLocked(state);
return succeeded -> onTuneCompleted(token, succeeded);
}
}
private void onTuneCompleted(@NonNull Object token, boolean succeeded) {
synchronized (mLock) {
if (mTuningToken != token) return;
mTuningToken = null;
notifyPlaybackStateLocked(succeeded
? PlaybackState.STATE_PLAYING : PlaybackState.STATE_ERROR);
}
}
/**
* Request audio stream muted or unmuted.
*
* @param muted true, if audio stream should be muted, false if unmuted
* @return true, if request has succeeded (maybe delayed)
*/
public boolean requestMuted(boolean muted) {
Log.v(TAG, "requestMuted(" + muted + ")");
synchronized (mLock) {
if (muted) {
if (mTuningToken == null) {
notifyPlaybackStateLocked(PlaybackState.STATE_STOPPED);
}
return abandonAudioFocusLocked();
} else {
if (!requestAudioFocusLocked()) return false;
if (mTuningToken == null) {
notifyPlaybackStateLocked(PlaybackState.STATE_PLAYING);
}
return true;
}
}
}
private void onAudioFocusChange(int focusChange) {
Log.v(TAG, "onAudioFocusChange(" + focusChange + ")");
synchronized (mLock) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
mHasSomeFocus = true;
// we assume that audio focus was requested only when we mean to unmute
unmuteLocked();
break;
case AudioManager.AUDIOFOCUS_LOSS:
Log.i(TAG, "Unexpected audio focus loss");
mHasSomeFocus = false;
mRadioTunerExt.setMuted(true);
notifyPlaybackStateLocked(PlaybackState.STATE_STOPPED);
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
mRadioTunerExt.setMuted(true);
break;
default:
Log.w(TAG, "Unexpected audio focus state: " + focusChange);
}
}
}
/**
* Dumps the current audio stream controller state
*/
public void dump(IndentingPrintWriter writer) {
writer.println("AudioStreamController");
writer.increaseIndent();
synchronized (mLock) {
writer.printf("Focus Request: %s\n", mGainFocusReq);
writer.printf("Has Some Focus: %b\n", mHasSomeFocus);
writer.printf("PlayBack State: %d\n", mCurrentPlaybackState);
writer.printf("Is Tuning Token Available: %s\n", mTuningToken != null);
}
writer.decreaseIndent();
}
}