• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.radio.platform;
18 
19 import android.content.Context;
20 import android.hardware.radio.ProgramList;
21 import android.hardware.radio.ProgramSelector;
22 import android.hardware.radio.RadioManager;
23 import android.hardware.radio.RadioTuner;
24 import android.media.AudioAttributes;
25 import android.media.AudioDeviceInfo;
26 import android.media.AudioManager;
27 import android.media.HwAudioSource;
28 import android.text.TextUtils;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 
33 import com.android.car.radio.util.Log;
34 
35 import java.util.Objects;
36 import java.util.stream.Stream;
37 
38 /**
39  * Proposed extensions to android.hardware.radio.RadioTuner.
40  *
41  * They might eventually get pushed to the framework.
42  */
43 public class RadioTunerExt {
44     private static final String TAG = "BcRadioApp.tunerext";
45 
46     private final Object mLock = new Object();
47     private final Object mTuneLock = new Object();
48     private final RadioTuner mTuner;
49 
50     private HwAudioSource mHwAudioSource;
51 
52     @Nullable private ProgramSelector mOperationSelector;  // null for seek operations
53     @Nullable private TuneCallback mOperationResultCb;
54 
55     /**
56      * A callback handling tune/seek operation result.
57      */
58     public interface TuneCallback {
59         /**
60          * Called when tune operation finished.
61          *
62          * @param succeeded States whether the operation succeeded or not.
63          */
onFinished(boolean succeeded)64         void onFinished(boolean succeeded);
65 
66         /**
67          * Chains other result callbacks.
68          */
alsoCall(@onNull TuneCallback other)69         default TuneCallback alsoCall(@NonNull TuneCallback other) {
70             return succeeded -> {
71                 onFinished(succeeded);
72                 other.onFinished(succeeded);
73             };
74         }
75     }
76 
RadioTunerExt(@onNull Context context, @NonNull RadioTuner tuner, @NonNull TunerCallbackAdapterExt cbExt)77     RadioTunerExt(@NonNull Context context, @NonNull RadioTuner tuner,
78             @NonNull TunerCallbackAdapterExt cbExt) {
79         mTuner = Objects.requireNonNull(tuner);
80         cbExt.setTuneFailedCallback(this::onTuneFailed);
81         cbExt.setProgramInfoCallback(this::onProgramInfoChanged);
82 
83         final AudioDeviceInfo tunerDevice = findTunerDevice(context, null);
84         if (tunerDevice == null) {
85             Log.e(TAG, "No TUNER_DEVICE found on board");
86         } else {
87             mHwAudioSource = new HwAudioSource.Builder()
88                 .setAudioDeviceInfo(tunerDevice)
89                 .setAudioAttributes(new AudioAttributes.Builder()
90                     .setUsage(AudioAttributes.USAGE_MEDIA)
91                     .build())
92                 .build();
93         }
94     }
95 
setMuted(boolean muted)96     public boolean setMuted(boolean muted) {
97         if (mHwAudioSource == null) {
98             Log.e(TAG, "No TUNER_DEVICE found on board");
99             return false;
100         }
101         synchronized (mLock) {
102             if (muted) {
103                 mHwAudioSource.stop();
104             } else {
105                 mHwAudioSource.start();
106             }
107             return true;
108         }
109     }
110 
111     /**
112      * See {@link RadioTuner#scan}.
113      */
seek(boolean forward, @Nullable TuneCallback resultCb)114     public void seek(boolean forward, @Nullable TuneCallback resultCb) {
115         synchronized (mTuneLock) {
116             synchronized (mLock) {
117                 markOperationFinishedLocked(false);
118                 mOperationResultCb = resultCb;
119             }
120 
121             mTuner.cancel();
122             int res = mTuner.scan(
123                     forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN, false);
124             if (res != RadioManager.STATUS_OK) {
125                 throw new RuntimeException("Seek failed with result of " + res);
126             }
127         }
128     }
129 
130     /**
131      * See {@link RadioTuner#step}.
132      */
step(boolean forward, @Nullable TuneCallback resultCb)133     public void step(boolean forward, @Nullable TuneCallback resultCb) {
134         synchronized (mTuneLock) {
135             markOperationFinishedLocked(false);
136             mOperationResultCb = resultCb;
137         }
138         mTuner.cancel();
139         int res =
140                 mTuner.step(forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN, false);
141         if (res != RadioManager.STATUS_OK) {
142             throw new RuntimeException("Step failed with result of " + res);
143         }
144     }
145 
146     /**
147      * See {@link RadioTuner#tune}.
148      */
tune(@onNull ProgramSelector selector, @Nullable TuneCallback resultCb)149     public void tune(@NonNull ProgramSelector selector, @Nullable TuneCallback resultCb) {
150         synchronized (mTuneLock) {
151             synchronized (mLock) {
152                 markOperationFinishedLocked(false);
153                 mOperationSelector = selector;
154                 mOperationResultCb = resultCb;
155             }
156 
157             mTuner.cancel();
158             mTuner.tune(selector);
159         }
160     }
161 
162     /**
163      * Get the {@link AudioDeviceInfo} instance with {@link AudioDeviceInfo#TYPE_FM_TUNER}
164      * by a given address. If the given address is null, returns the first found one.
165      */
findTunerDevice(Context context, @Nullable String address)166     private AudioDeviceInfo findTunerDevice(Context context, @Nullable String address) {
167         AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
168         AudioDeviceInfo[] devices = am.getDevices(AudioManager.GET_DEVICES_INPUTS);
169         for (AudioDeviceInfo device : devices) {
170             if (device.getType() == AudioDeviceInfo.TYPE_FM_TUNER) {
171                 if (TextUtils.isEmpty(address) || address.equals(device.getAddress())) {
172                     return device;
173                 }
174             }
175         }
176         return null;
177     }
178 
markOperationFinishedLocked(boolean succeeded)179     private void markOperationFinishedLocked(boolean succeeded) {
180         if (mOperationResultCb == null) return;
181 
182         if (Log.isLoggable(TAG, Log.DEBUG)) {
183             Log.d(TAG, "Tune operation for " + mOperationSelector
184                     + (succeeded ? " succeeded" : " failed"));
185         }
186 
187         TuneCallback cb = mOperationResultCb;
188         mOperationSelector = null;
189         mOperationResultCb = null;
190 
191         cb.onFinished(succeeded);
192 
193         if (mOperationSelector != null) {
194             throw new IllegalStateException("Can't tune in callback's failed branch. It might "
195                     + "interfere with tune operation that requested current one cancellation");
196         }
197     }
198 
isMatching(@onNull ProgramSelector currentOperation, @NonNull ProgramSelector event)199     private boolean isMatching(@NonNull ProgramSelector currentOperation,
200             @NonNull ProgramSelector event) {
201         ProgramSelector.Identifier pri = currentOperation.getPrimaryId();
202         return Stream.of(event.getAllIds(pri.getType())).anyMatch(id -> pri.equals(id));
203     }
204 
onProgramInfoChanged(RadioManager.ProgramInfo info)205     private void onProgramInfoChanged(RadioManager.ProgramInfo info) {
206         synchronized (mLock) {
207             if (mOperationResultCb == null) return;
208             // if we're seeking, all program info chanes does match
209             if (mOperationSelector != null) {
210                 if (!isMatching(mOperationSelector, info.getSelector())) return;
211             }
212             markOperationFinishedLocked(true);
213         }
214     }
215 
onTuneFailed(int result, @Nullable ProgramSelector selector)216     private void onTuneFailed(int result, @Nullable ProgramSelector selector) {
217         synchronized (mLock) {
218             if (mOperationResultCb == null) return;
219             // if we're seeking and got a failed tune (or vice versa), that's a mismatch
220             if ((mOperationSelector == null) != (selector == null)) return;
221             if (mOperationSelector != null) {
222                 if (!isMatching(mOperationSelector, selector)) return;
223             }
224             markOperationFinishedLocked(false);
225         }
226     }
227 
228     /**
229      * See {@link RadioTuner#cancel}.
230      */
cancel()231     public void cancel() {
232         synchronized (mTuneLock) {
233             synchronized (mLock) {
234                 markOperationFinishedLocked(false);
235             }
236 
237             int res = mTuner.cancel();
238             if (res != RadioManager.STATUS_OK) {
239                 Log.e(TAG, "Cancel failed with result of " + res);
240             }
241         }
242     }
243 
244     /**
245      * See {@link RadioTuner#getDynamicProgramList}.
246      */
getDynamicProgramList(@ullable ProgramList.Filter filter)247     public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
248         return mTuner.getDynamicProgramList(filter);
249     }
250 
close()251     public void close() {
252         synchronized (mLock) {
253             markOperationFinishedLocked(false);
254         }
255 
256         if (mHwAudioSource != null) {
257             mHwAudioSource.stop();
258             mHwAudioSource = null;
259         }
260         mTuner.close();
261     }
262 }
263