blob: d081d5ef0d239b99f3e26ddd00c25b121dc12130 [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.platform;
import android.content.Context;
import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioTuner;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.HwAudioSource;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.android.car.radio.util.Log;
import com.android.internal.annotations.GuardedBy;
import java.util.Objects;
import java.util.stream.Stream;
/**
* Proposed extensions to android.hardware.radio.RadioTuner.
*
* They might eventually get pushed to the framework.
*/
public final class RadioTunerExt {
private static final String TAG = "BcRadioApp.tunerext";
private final Object mLock = new Object();
private final RadioTuner mTuner;
@GuardedBy("mLock")
private HwAudioSource mHwAudioSource;
@GuardedBy("mLock")
@Nullable private ProgramSelector mOperationSelector; // null for seek operations
@GuardedBy("mLock")
@Nullable private TuneCallback mOperationResultCb;
/**
* A callback handling tune/seek operation result.
*/
public interface TuneCallback {
/**
* Called when tune operation finished.
*
* @param succeeded States whether the operation succeeded or not.
*/
void onFinished(boolean succeeded);
/**
* Chains other result callbacks.
*/
default TuneCallback alsoCall(TuneCallback other) {
return succeeded -> {
onFinished(succeeded);
other.onFinished(succeeded);
};
}
}
RadioTunerExt(Context context, RadioTuner tuner, TunerCallbackAdapterExt cbExt) {
mTuner = Objects.requireNonNull(tuner, "Tuner cannot be null");
cbExt.setTuneFailedCallback(this::onTuneFailed);
cbExt.setProgramInfoCallback(this::onProgramInfoChanged);
AudioDeviceInfo tunerDevice = findTunerDevice(context, /* address= */ null);
if (tunerDevice == null) {
Log.e(TAG, "No TUNER_DEVICE found on board");
} else {
mHwAudioSource = new HwAudioSource.Builder()
.setAudioDeviceInfo(tunerDevice)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.build())
.build();
}
}
public boolean setMuted(boolean muted) {
synchronized (mLock) {
if (mHwAudioSource == null) {
Log.e(TAG, "No TUNER_DEVICE found on board when setting muted");
return false;
}
if (muted) {
mHwAudioSource.stop();
} else {
mHwAudioSource.start();
}
return true;
}
}
/**
* See {@link RadioTuner#scan}.
*/
public void seek(boolean forward, @Nullable TuneCallback resultCb) {
synchronized (mLock) {
markOperationFinishedLocked(/* succeeded= */ false);
mOperationResultCb = resultCb;
}
int res = mTuner.scan(
forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN,
/* skipSubChannel= */ false);
if (res != RadioManager.STATUS_OK) {
throw new RuntimeException("Seek failed with result of " + res);
}
}
/**
* See {@link RadioTuner#step}.
*/
public void step(boolean forward, @Nullable TuneCallback resultCb) {
synchronized (mLock) {
markOperationFinishedLocked(/* succeeded= */ false);
mOperationResultCb = resultCb;
}
int res =
mTuner.step(forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN,
/* skipSubChannel= */ false);
if (res != RadioManager.STATUS_OK) {
throw new RuntimeException("Step failed with result of " + res);
}
}
/**
* See {@link RadioTuner#tune}.
*/
public void tune(ProgramSelector selector, @Nullable TuneCallback resultCb) {
synchronized (mLock) {
markOperationFinishedLocked(/* succeeded= */ false);
mOperationSelector = selector;
mOperationResultCb = resultCb;
}
mTuner.tune(selector);
}
/**
* Get the {@link AudioDeviceInfo} instance with {@link AudioDeviceInfo#TYPE_FM_TUNER}
* by a given address. If the given address is null, returns the first found one.
*/
private AudioDeviceInfo findTunerDevice(Context context, @Nullable String address) {
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioDeviceInfo[] devices = am.getDevices(AudioManager.GET_DEVICES_INPUTS);
for (AudioDeviceInfo device : devices) {
if (device.getType() == AudioDeviceInfo.TYPE_FM_TUNER) {
if (TextUtils.isEmpty(address) || address.equals(device.getAddress())) {
return device;
}
}
}
return null;
}
@GuardedBy("mLock")
private void markOperationFinishedLocked(boolean succeeded) {
if (mOperationResultCb == null) {
return;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Tune operation for " + mOperationSelector
+ (succeeded ? " succeeded" : " failed"));
}
TuneCallback cb = mOperationResultCb;
mOperationSelector = null;
mOperationResultCb = null;
cb.onFinished(succeeded);
if (mOperationSelector != null) {
throw new IllegalStateException("Can't tune in callback's failed branch. It might "
+ "interfere with tune operation that requested current one cancellation");
}
}
private boolean isMatching(ProgramSelector currentOperation, ProgramSelector event) {
ProgramSelector.Identifier pri = currentOperation.getPrimaryId();
return Stream.of(event.getAllIds(pri.getType())).anyMatch(id -> pri.equals(id));
}
private void onProgramInfoChanged(RadioManager.ProgramInfo info) {
synchronized (mLock) {
if (mOperationResultCb == null) {
return;
}
// if we're seeking, all program info chanes does match
if (mOperationSelector != null) {
if (!isMatching(mOperationSelector, info.getSelector())) {
return;
}
}
markOperationFinishedLocked(/* succeeded= */ true);
}
}
private void onTuneFailed(int result, @Nullable ProgramSelector selector) {
synchronized (mLock) {
if (mOperationResultCb == null) {
return;
}
// if we're seeking and got a failed tune (or vice versa), that's a mismatch
if ((mOperationSelector == null) != (selector == null)) {
return;
}
if (mOperationSelector != null) {
if (!isMatching(mOperationSelector, selector)) {
return;
}
}
markOperationFinishedLocked(/* succeeded= */ false);
}
}
/**
* See {@link RadioTuner#cancel}.
*/
public void cancel() {
synchronized (mLock) {
markOperationFinishedLocked(/* succeeded= */ false);
}
int res = mTuner.cancel();
if (res != RadioManager.STATUS_OK) {
Log.e(TAG, "Cancel failed with result of " + res);
}
}
/**
* See {@link RadioTuner#getDynamicProgramList}.
*/
@Nullable
public ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
return mTuner.getDynamicProgramList(filter);
}
public void close() {
synchronized (mLock) {
markOperationFinishedLocked(/* succeeded= */ false);
if (mHwAudioSource != null) {
mHwAudioSource.stop();
mHwAudioSource = null;
}
}
mTuner.close();
}
}