1 /* 2 * Copyright (c) 2016, 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 package com.android.car.radio.demo; 17 18 import android.car.hardware.radio.CarRadioManager; 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.hardware.radio.RadioManager; 22 import android.media.AudioAttributes; 23 import android.media.AudioManager; 24 import android.os.RemoteException; 25 import android.os.SystemProperties; 26 import android.support.car.Car; 27 import android.support.car.CarNotConnectedException; 28 import android.support.car.CarConnectionCallback; 29 import android.support.car.media.CarAudioManager; 30 import android.util.Log; 31 import com.android.car.radio.service.IRadioCallback; 32 import com.android.car.radio.service.IRadioManager; 33 import com.android.car.radio.service.RadioStation; 34 35 import java.util.ArrayList; 36 import java.util.List; 37 38 /** 39 * A demo {@link IRadiomanager} that has a fixed set of AM and FM stations. 40 */ 41 public class RadioDemo implements AudioManager.OnAudioFocusChangeListener { 42 private static final String TAG = "RadioDemo"; 43 44 /** 45 * The property name to enable demo mode. 46 */ 47 public static final String DEMO_MODE_PROPERTY = "com.android.car.radio.demo"; 48 49 /** 50 * The property name to enable the radio in demo mode with dual tuners. 51 */ 52 public static final String DUAL_DEMO_MODE_PROPERTY = "com.android.car.radio.demo.dual"; 53 54 private static RadioDemo sInstance; 55 private List<IRadioCallback> mCallbacks = new ArrayList<>(); 56 57 private List<RadioStation> mCurrentStations = new ArrayList<>(); 58 private int mCurrentRadioBand = RadioManager.BAND_FM; 59 60 private Car mCarApi; 61 private CarAudioManager mCarAudioManager; 62 private AudioAttributes mRadioAudioAttributes; 63 64 private boolean mHasAudioFocus; 65 66 private int mCurrentIndex; 67 private boolean mIsMuted; 68 69 private RadioDemo(Context context) { 70 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { 71 mCarApi = Car.createCar(context, mCarConnectionCallback); 72 mCarApi.connect(); 73 } 74 } 75 76 /** 77 * Returns a mock {@link IRadioManager} to use for demo purposes. The returned class will have 78 * a fixed list of AM and FM changegs and support all the IRadioManager's functionality. 79 */ 80 public IRadioManager.Stub createDemoManager() { 81 return new IRadioManager.Stub() { 82 @Override 83 public void tune(RadioStation station) throws RemoteException { 84 if (station == null || !requestAudioFocus()) { 85 return; 86 } 87 88 if (station.getRadioBand() != mCurrentRadioBand) { 89 switchRadioBand(station.getRadioBand()); 90 } 91 92 boolean found = false; 93 94 for (int i = 0, size = mCurrentStations.size(); i < size; i++) { 95 RadioStation storedStation = mCurrentStations.get(i); 96 97 if (storedStation.equals(station)) { 98 found = true; 99 mCurrentIndex = i; 100 break; 101 } 102 } 103 104 // If not found, then insert it into the list, sorted by the channel. 105 if (!found) { 106 int indexToInsert = 0; 107 108 for (int i = 0, size = mCurrentStations.size(); i < size; i++) { 109 RadioStation storedStation = mCurrentStations.get(i); 110 111 if (station.getChannelNumber() >= storedStation.getChannelNumber()) { 112 indexToInsert = i + 1; 113 break; 114 } 115 } 116 117 RadioStation stationToInsert = new RadioStation(station.getChannelNumber(), 118 0 /* subChannel */, station.getRadioBand(), null /* rds */); 119 mCurrentStations.add(indexToInsert, stationToInsert); 120 121 mCurrentIndex = indexToInsert; 122 } 123 124 notifyCallbacks(station); 125 } 126 127 @Override 128 public void seekForward() throws RemoteException { 129 if (!requestAudioFocus()) { 130 return; 131 } 132 133 if (++mCurrentIndex >= mCurrentStations.size()) { 134 mCurrentIndex = 0; 135 } 136 137 notifyCallbacks(mCurrentStations.get(mCurrentIndex)); 138 } 139 140 @Override 141 public void seekBackward() throws RemoteException { 142 if (!requestAudioFocus()) { 143 return; 144 } 145 146 if (--mCurrentIndex < 0){ 147 mCurrentIndex = mCurrentStations.size() - 1; 148 } 149 150 notifyCallbacks(mCurrentStations.get(mCurrentIndex)); 151 } 152 153 @Override 154 public boolean mute() throws RemoteException { 155 mIsMuted = true; 156 notifyCallbacksMuteChanged(mIsMuted); 157 return mIsMuted; 158 } 159 160 @Override 161 public boolean unMute() throws RemoteException { 162 requestAudioFocus(); 163 164 if (mHasAudioFocus) { 165 mIsMuted = false; 166 } 167 168 notifyCallbacksMuteChanged(mIsMuted); 169 return !mIsMuted; 170 } 171 172 @Override 173 public boolean isMuted() throws RemoteException { 174 return mIsMuted; 175 } 176 177 @Override 178 public int openRadioBand(int radioBand) throws RemoteException { 179 if (!requestAudioFocus()) { 180 return RadioManager.STATUS_ERROR; 181 } 182 183 switchRadioBand(radioBand); 184 notifyCallbacks(radioBand); 185 return RadioManager.STATUS_OK; 186 } 187 188 @Override 189 public void addRadioTunerCallback(IRadioCallback callback) throws RemoteException { 190 mCallbacks.add(callback); 191 } 192 193 @Override 194 public void removeRadioTunerCallback(IRadioCallback callback) throws RemoteException { 195 mCallbacks.remove(callback); 196 } 197 198 @Override 199 public RadioStation getCurrentRadioStation() throws RemoteException { 200 return mCurrentStations.get(mCurrentIndex); 201 } 202 203 @Override 204 public boolean isInitialized() throws RemoteException { 205 return true; 206 } 207 208 @Override 209 public boolean hasFocus() { 210 return mHasAudioFocus; 211 } 212 213 @Override 214 public boolean hasDualTuners() throws RemoteException { 215 return SystemProperties.getBoolean(RadioDemo.DUAL_DEMO_MODE_PROPERTY, false); 216 } 217 }; 218 } 219 220 @Override 221 public void onAudioFocusChange(int focusChange) { 222 if (Log.isLoggable(TAG, Log.DEBUG)) { 223 Log.d(TAG, "focus change: " + focusChange); 224 } 225 226 switch (focusChange) { 227 case AudioManager.AUDIOFOCUS_GAIN: 228 mHasAudioFocus = true; 229 break; 230 231 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 232 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 233 mHasAudioFocus = false; 234 break; 235 236 case AudioManager.AUDIOFOCUS_LOSS: 237 abandonAudioFocus(); 238 break; 239 240 default: 241 // Do nothing for all other cases. 242 } 243 } 244 245 /** 246 * Requests audio focus for the current application. 247 * 248 * @return {@code true} if the request succeeded. 249 */ 250 private boolean requestAudioFocus() { 251 if (mCarAudioManager == null) { 252 return false; 253 } 254 255 int status = AudioManager.AUDIOFOCUS_REQUEST_FAILED; 256 try { 257 status = mCarAudioManager.requestAudioFocus(this, mRadioAudioAttributes, 258 AudioManager.AUDIOFOCUS_GAIN, 0); 259 } catch (CarNotConnectedException e) { 260 Log.e(TAG, "requestAudioFocus() failed", e); 261 } 262 263 if (Log.isLoggable(TAG, Log.DEBUG)) { 264 Log.d(TAG, "requestAudioFocus status: " + status); 265 } 266 267 if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 268 mHasAudioFocus = true; 269 } 270 271 return status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 272 } 273 274 /** 275 * Abandons audio focus for the current application. 276 * 277 * @return {@code true} if the request succeeded. 278 */ 279 private void abandonAudioFocus() { 280 if (Log.isLoggable(TAG, Log.DEBUG)) { 281 Log.d(TAG, "abandonAudioFocus()"); 282 } 283 284 if (mCarAudioManager == null) { 285 return; 286 } 287 288 mCarAudioManager.abandonAudioFocus(this, mRadioAudioAttributes); 289 } 290 291 /** 292 * {@link CarConnectionCallback} that retrieves the {@link CarRadioManager}. 293 */ 294 private final CarConnectionCallback mCarConnectionCallback = 295 new CarConnectionCallback() { 296 @Override 297 public void onConnected(Car car) { 298 if (Log.isLoggable(TAG, Log.DEBUG)) { 299 Log.d(TAG, "Car service connected."); 300 } 301 try { 302 // The CarAudioManager only needs to be retrieved once. 303 if (mCarAudioManager == null) { 304 mCarAudioManager = (CarAudioManager) mCarApi.getCarManager( 305 android.car.Car.AUDIO_SERVICE); 306 307 mRadioAudioAttributes = mCarAudioManager.getAudioAttributesForCarUsage( 308 CarAudioManager.CAR_AUDIO_USAGE_RADIO); 309 } 310 } catch (CarNotConnectedException e) { 311 //TODO finish 312 Log.e(TAG, "Car not connected"); 313 } 314 } 315 316 @Override 317 public void onDisconnected(Car car) { 318 if (Log.isLoggable(TAG, Log.DEBUG)) { 319 Log.d(TAG, "Car service disconnected."); 320 } 321 } 322 }; 323 324 /** 325 * Switches to the corresponding radio band. This will update the list of current stations 326 * as well as notify any callbacks. 327 */ 328 private void switchRadioBand(int radioBand) { 329 switch (radioBand) { 330 case RadioManager.BAND_AM: 331 mCurrentStations = DemoRadioStations.getAmStations(); 332 break; 333 case RadioManager.BAND_FM: 334 mCurrentStations = DemoRadioStations.getFmStations(); 335 break; 336 default: 337 mCurrentStations = new ArrayList<>(); 338 } 339 340 mCurrentRadioBand = radioBand; 341 mCurrentIndex = 0; 342 343 notifyCallbacks(mCurrentRadioBand); 344 notifyCallbacks(mCurrentStations.get(mCurrentIndex)); 345 } 346 347 /** 348 * Notifies any {@link IRadioCallback} that the mute state of the radio has changed. 349 */ 350 private void notifyCallbacksMuteChanged(boolean isMuted) { 351 for (IRadioCallback callback : mCallbacks) { 352 try { 353 callback.onRadioMuteChanged(isMuted); 354 } catch (RemoteException e) { 355 // Ignore. 356 } 357 } 358 } 359 360 /** 361 * Notifies any {@link IRadioCallback}s that the radio band has changed. 362 */ 363 private void notifyCallbacks(int radioBand) { 364 for (IRadioCallback callback : mCallbacks) { 365 try { 366 callback.onRadioBandChanged(radioBand); 367 } catch (RemoteException e) { 368 // Ignore. 369 } 370 } 371 } 372 373 /** 374 * Notifies any {@link IRadioCallback}s that the radio station has been changed to the given 375 * {@link RadioStation}. 376 */ 377 private void notifyCallbacks(RadioStation station) { 378 for (IRadioCallback callback : mCallbacks) { 379 try { 380 callback.onRadioStationChanged(station); 381 } catch (RemoteException e) { 382 // Ignore. 383 } 384 } 385 } 386 387 public static RadioDemo getInstance(Context context) { 388 if (sInstance == null) { 389 sInstance = new RadioDemo(context); 390 } 391 392 return sInstance; 393 } 394 } 395