• 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.Nullable;
31 
32 import com.android.car.radio.util.Log;
33 import com.android.internal.annotations.GuardedBy;
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 final class RadioTunerExt {
44     private static final String TAG = "BcRadioApp.tunerext";
45 
46     private final Object mLock = new Object();
47     private final RadioTuner mTuner;
48 
49     @GuardedBy("mLock")
50     private HwAudioSource mHwAudioSource;
51 
52     @GuardedBy("mLock")
53     @Nullable private ProgramSelector mOperationSelector;  // null for seek operations
54 
55     @GuardedBy("mLock")
56     @Nullable private TuneCallback mOperationResultCb;
57 
58     /**
59      * A callback handling tune/seek operation result.
60      */
61     public interface TuneCallback {
62         /**
63          * Called when tune operation finished.
64          *
65          * @param succeeded States whether the operation succeeded or not.
66          */
onFinished(boolean succeeded)67         void onFinished(boolean succeeded);
68 
69         /**
70          * Chains other result callbacks.
71          */
alsoCall(TuneCallback other)72         default TuneCallback alsoCall(TuneCallback other) {
73             return succeeded -> {
74                 onFinished(succeeded);
75                 other.onFinished(succeeded);
76             };
77         }
78     }
79 
RadioTunerExt(Context context, RadioTuner tuner, TunerCallbackAdapterExt cbExt)80     RadioTunerExt(Context context, RadioTuner tuner, TunerCallbackAdapterExt cbExt) {
81         mTuner = Objects.requireNonNull(tuner, "Tuner cannot be null");
82         cbExt.setTuneFailedCallback(this::onTuneFailed);
83         cbExt.setProgramInfoCallback(this::onProgramInfoChanged);
84 
85         AudioDeviceInfo tunerDevice = findTunerDevice(context, /* address= */ null);
86         if (tunerDevice == null) {
87             Log.e(TAG, "No TUNER_DEVICE found on board");
88         } else {
89             mHwAudioSource = new HwAudioSource.Builder()
90                 .setAudioDeviceInfo(tunerDevice)
91                 .setAudioAttributes(new AudioAttributes.Builder()
92                     .setUsage(AudioAttributes.USAGE_MEDIA)
93                     .build())
94                 .build();
95         }
96     }
97 
setMuted(boolean muted)98     public boolean setMuted(boolean muted) {
99         synchronized (mLock) {
100             if (mHwAudioSource == null) {
101                 Log.e(TAG, "No TUNER_DEVICE found on board when setting muted");
102                 return false;
103             }
104             if (muted) {
105                 mHwAudioSource.stop();
106             } else {
107                 mHwAudioSource.start();
108             }
109             return true;
110         }
111     }
112 
113     /**
114      * See {@link RadioTuner#scan}.
115      */
seek(boolean forward, @Nullable TuneCallback resultCb)116     public void seek(boolean forward, @Nullable TuneCallback resultCb) {
117         synchronized (mLock) {
118             markOperationFinishedLocked(/* succeeded= */ false);
119             mOperationResultCb = resultCb;
120         }
121 
122         int res = mTuner.scan(
123                 forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN,
124                 /* skipSubChannel= */ false);
125         if (res != RadioManager.STATUS_OK) {
126             throw new RuntimeException("Seek failed with result of " + res);
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 (mLock) {
135             markOperationFinishedLocked(/* succeeded= */ false);
136             mOperationResultCb = resultCb;
137         }
138 
139         int res =
140                 mTuner.step(forward ? RadioTuner.DIRECTION_UP : RadioTuner.DIRECTION_DOWN,
141                         /* skipSubChannel= */ false);
142         if (res != RadioManager.STATUS_OK) {
143             throw new RuntimeException("Step failed with result of " + res);
144         }
145     }
146 
147     /**
148      * See {@link RadioTuner#tune}.
149      */
tune(ProgramSelector selector, @Nullable TuneCallback resultCb)150     public void tune(ProgramSelector selector, @Nullable TuneCallback resultCb) {
151         synchronized (mLock) {
152             markOperationFinishedLocked(/* succeeded= */ false);
153             mOperationSelector = selector;
154             mOperationResultCb = resultCb;
155         }
156 
157         mTuner.tune(selector);
158     }
159 
160     /**
161      * Get the {@link AudioDeviceInfo} instance with {@link AudioDeviceInfo#TYPE_FM_TUNER}
162      * by a given address. If the given address is null, returns the first found one.
163      */
findTunerDevice(Context context, @Nullable String address)164     private AudioDeviceInfo findTunerDevice(Context context, @Nullable String address) {
165         AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
166         AudioDeviceInfo[] devices = am.getDevices(AudioManager.GET_DEVICES_INPUTS);
167         for (AudioDeviceInfo device : devices) {
168             if (device.getType() == AudioDeviceInfo.TYPE_FM_TUNER) {
169                 if (TextUtils.isEmpty(address) || address.equals(device.getAddress())) {
170                     return device;
171                 }
172             }
173         }
174         return null;
175     }
176 
177     @GuardedBy("mLock")
markOperationFinishedLocked(boolean succeeded)178     private void markOperationFinishedLocked(boolean succeeded) {
179         if (mOperationResultCb == null) {
180             return;
181         }
182 
183         if (Log.isLoggable(TAG, Log.DEBUG)) {
184             Log.d(TAG, "Tune operation for " + mOperationSelector
185                     + (succeeded ? " succeeded" : " failed"));
186         }
187 
188         TuneCallback cb = mOperationResultCb;
189         mOperationSelector = null;
190         mOperationResultCb = null;
191 
192         cb.onFinished(succeeded);
193 
194         if (mOperationSelector != null) {
195             throw new IllegalStateException("Can't tune in callback's failed branch. It might "
196                     + "interfere with tune operation that requested current one cancellation");
197         }
198     }
199 
isMatching(ProgramSelector currentOperation, ProgramSelector event)200     private boolean isMatching(ProgramSelector currentOperation, 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) {
208                 return;
209             }
210             // if we're seeking, all program info chanes does match
211             if (mOperationSelector != null) {
212                 if (!isMatching(mOperationSelector, info.getSelector())) {
213                     return;
214                 }
215             }
216             markOperationFinishedLocked(/* succeeded= */ true);
217         }
218     }
219 
onTuneFailed(int result, @Nullable ProgramSelector selector)220     private void onTuneFailed(int result, @Nullable ProgramSelector selector) {
221         synchronized (mLock) {
222             if (mOperationResultCb == null) {
223                 return;
224             }
225             // if we're seeking and got a failed tune (or vice versa), that's a mismatch
226             if ((mOperationSelector == null) != (selector == null)) {
227                 return;
228             }
229             if (mOperationSelector != null) {
230                 if (!isMatching(mOperationSelector, selector)) {
231                     return;
232                 }
233             }
234             markOperationFinishedLocked(/* succeeded= */ false);
235         }
236     }
237 
238     /**
239      * See {@link RadioTuner#cancel}.
240      */
cancel()241     public void cancel() {
242         synchronized (mLock) {
243             markOperationFinishedLocked(/* succeeded= */ false);
244         }
245 
246         int res = mTuner.cancel();
247         if (res != RadioManager.STATUS_OK) {
248             Log.e(TAG, "Cancel failed with result of " + res);
249         }
250     }
251 
252     /**
253      * See {@link RadioTuner#getDynamicProgramList}.
254      */
255     @Nullable
getDynamicProgramList(@ullable ProgramList.Filter filter)256     public ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
257         return mTuner.getDynamicProgramList(filter);
258     }
259 
close()260     public void close() {
261         synchronized (mLock) {
262             markOperationFinishedLocked(/* succeeded= */ false);
263             if (mHwAudioSource != null) {
264                 mHwAudioSource.stop();
265                 mHwAudioSource = null;
266             }
267         }
268 
269         mTuner.close();
270     }
271 }
272