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 17package com.android.car.stream.radio; 18 19import android.content.BroadcastReceiver; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.content.IntentFilter; 24import android.content.ServiceConnection; 25import android.os.Handler; 26import android.os.IBinder; 27import android.os.RemoteException; 28import android.util.Log; 29import com.android.car.radio.service.IRadioCallback; 30import com.android.car.radio.service.IRadioManager; 31import com.android.car.radio.service.RadioRds; 32import com.android.car.radio.service.RadioStation; 33import com.android.car.stream.R; 34import com.android.car.stream.StreamProducer; 35 36/** 37 * A {@link StreamProducer} that will connect to the {@link IRadioManager} and produce cards 38 * corresponding to the currently playing radio station. 39 */ 40public class RadioStreamProducer extends StreamProducer { 41 private static final String TAG = "RadioStreamProducer"; 42 43 /** 44 * The amount of time to wait before re-trying to connect to {@link IRadioManager}. 45 */ 46 private static final int SERVICE_CONNECTION_RETRY_DELAY_MS = 5000; 47 48 // Radio actions that are used by broadcasts that occur on interaction with the radio card. 49 static final int ACTION_SEEK_FORWARD = 1; 50 static final int ACTION_SEEK_BACKWARD = 2; 51 static final int ACTION_PAUSE = 3; 52 static final int ACTION_PLAY = 4; 53 static final int ACTION_STOP = 5; 54 55 /** 56 * The action in an {@link Intent} that is meant to effect certain radio actions. 57 */ 58 static final String RADIO_INTENT_ACTION = 59 "com.android.car.stream.radio.RADIO_INTENT_ACTION"; 60 61 /** 62 * The extra within the {@link Intent} that points to the specific action to be taken on the 63 * radio. 64 */ 65 static final String RADIO_ACTION_EXTRA = "radio_action_extra"; 66 67 private final Handler mHandler = new Handler(); 68 69 private IRadioManager mRadioManager; 70 private RadioActionReceiver mReceiver; 71 private final RadioConverter mConverter; 72 73 /** 74 * The number of times that this stream producer has attempted to reconnect to the 75 * {@link IRadioManager} after a failure to bind. 76 */ 77 private int mConnectionRetryCount; 78 79 private int mCurrentChannelNumber; 80 private int mCurrentBand; 81 82 public RadioStreamProducer(Context context) { 83 super(context); 84 mConverter = new RadioConverter(context); 85 } 86 87 @Override 88 public void start() { 89 super.start(); 90 91 mReceiver = new RadioActionReceiver(); 92 mContext.registerReceiver(mReceiver, new IntentFilter(RADIO_INTENT_ACTION)); 93 94 bindRadioService(); 95 } 96 97 @Override 98 public void stop() { 99 if (Log.isLoggable(TAG, Log.DEBUG)) { 100 Log.d(TAG, "stop()"); 101 } 102 103 mHandler.removeCallbacks(mServiceConnectionRetry); 104 105 mContext.unregisterReceiver(mReceiver); 106 mReceiver = null; 107 108 mContext.unbindService(mServiceConnection); 109 super.stop(); 110 } 111 112 /** 113 * Binds to the RadioService and returns {@code true} if the connection was successful. 114 */ 115 private boolean bindRadioService() { 116 Intent radioService = new Intent(); 117 radioService.setComponent(new ComponentName( 118 mContext.getString(R.string.car_radio_component_package), 119 mContext.getString(R.string.car_radio_component_service))); 120 121 boolean bound = 122 !mContext.bindService(radioService, mServiceConnection, Context.BIND_AUTO_CREATE); 123 124 if (Log.isLoggable(TAG, Log.DEBUG)) { 125 Log.d(TAG, "bindRadioService(). Connected to radio service: " + bound); 126 } 127 128 return bound; 129 } 130 131 /** 132 * A {@link BroadcastReceiver} that listens for Intents that have the action 133 * {@link #RADIO_INTENT_ACTION} and corresponding parses the action event within it to effect 134 * radio playback. 135 */ 136 private class RadioActionReceiver extends BroadcastReceiver { 137 @Override 138 public void onReceive(Context context, Intent intent) { 139 if (mRadioManager == null || !RADIO_INTENT_ACTION.equals(intent.getAction())) { 140 return; 141 } 142 143 int radioAction = intent.getIntExtra(RADIO_ACTION_EXTRA, -1); 144 if (radioAction == -1) { 145 return; 146 } 147 148 switch (radioAction) { 149 case ACTION_SEEK_FORWARD: 150 try { 151 mRadioManager.seekForward(); 152 } catch (RemoteException e) { 153 Log.e(TAG, "Seek forward exception: " + e.getMessage()); 154 } 155 break; 156 157 case ACTION_SEEK_BACKWARD: 158 try { 159 mRadioManager.seekBackward(); 160 } catch (RemoteException e) { 161 Log.e(TAG, "Seek backward exception: " + e.getMessage()); 162 } 163 break; 164 165 case ACTION_PLAY: 166 try { 167 mRadioManager.unMute(); 168 } catch (RemoteException e) { 169 Log.e(TAG, "Radio play exception: " + e.getMessage()); 170 } 171 break; 172 173 case ACTION_STOP: 174 case ACTION_PAUSE: 175 try { 176 mRadioManager.mute(); 177 } catch (RemoteException e) { 178 Log.e(TAG, "Radio pause exception: " + e.getMessage()); 179 } 180 break; 181 182 default: 183 // Do nothing. 184 } 185 } 186 } 187 188 /** 189 * A {@link IRadioCallback} that will be notified of various state changes in the radio station. 190 * Upon these changes, it will push a new {@link com.android.car.stream.StreamCard} to the 191 * Stream service. 192 */ 193 private final IRadioCallback.Stub mCallback = new IRadioCallback.Stub() { 194 @Override 195 public void onRadioStationChanged(RadioStation station) { 196 if (Log.isLoggable(TAG, Log.DEBUG)) { 197 Log.d(TAG, "onRadioStationChanged: " + station); 198 } 199 200 mCurrentBand = station.getRadioBand(); 201 mCurrentChannelNumber = station.getChannelNumber(); 202 203 if (mRadioManager == null) { 204 return; 205 } 206 207 try { 208 boolean isPlaying = !mRadioManager.isMuted(); 209 postCard(mConverter.convert(station, isPlaying)); 210 } catch (RemoteException e) { 211 Log.e(TAG, "Post radio station changed error: " + e.getMessage()); 212 } 213 } 214 215 @Override 216 public void onRadioMetadataChanged(RadioRds rds) { 217 if (Log.isLoggable(TAG, Log.DEBUG)) { 218 Log.d(TAG, "onRadioMetadataChanged: " + rds); 219 } 220 221 // Ignore metadata changes because this will overwhelm the notifications. Instead, 222 // Only display the metadata that is retrieved in onRadioStationChanged(). 223 } 224 225 @Override 226 public void onRadioBandChanged(int radioBand) { 227 if (Log.isLoggable(TAG, Log.DEBUG)) { 228 Log.d(TAG, "onRadioBandChanged: " + radioBand); 229 } 230 231 if (mRadioManager == null) { 232 return; 233 } 234 235 try { 236 RadioStation station = new RadioStation(mCurrentChannelNumber, 237 0 /* subChannelNumber */, mCurrentBand, null /* rds */); 238 boolean isPlaying = !mRadioManager.isMuted(); 239 240 postCard(mConverter.convert(station, isPlaying)); 241 } catch (RemoteException e) { 242 Log.e(TAG, "Post radio station changed error: " + e.getMessage()); 243 } 244 } 245 246 @Override 247 public void onRadioMuteChanged(boolean isMuted) { 248 if (Log.isLoggable(TAG, Log.DEBUG)) { 249 Log.d(TAG, "onRadioMuteChanged(): " + isMuted); 250 } 251 252 RadioStation station = new RadioStation(mCurrentChannelNumber, 253 0 /* subChannelNumber */, mCurrentBand, null /* rds */); 254 255 postCard(mConverter.convert(station, !isMuted)); 256 } 257 258 @Override 259 public void onError(int status) { 260 Log.e(TAG, "Radio error: " + status); 261 } 262 }; 263 264 private ServiceConnection mServiceConnection = new ServiceConnection() { 265 @Override 266 public void onServiceConnected(ComponentName name, IBinder binder) { 267 mConnectionRetryCount = 0; 268 269 mRadioManager = IRadioManager.Stub.asInterface(binder); 270 271 if (Log.isLoggable(TAG, Log.DEBUG)) { 272 Log.d(TAG, "onSeviceConnected(): " + mRadioManager); 273 } 274 275 try { 276 mRadioManager.addRadioTunerCallback(mCallback); 277 278 if (mRadioManager.isInitialized() && mRadioManager.hasFocus()) { 279 boolean isPlaying = !mRadioManager.isMuted(); 280 postCard(mConverter.convert(mRadioManager.getCurrentRadioStation(), isPlaying)); 281 } 282 } catch (RemoteException e) { 283 Log.e(TAG, "addRadioTunerCallback() error: " + e.getMessage()); 284 } 285 } 286 287 @Override 288 public void onServiceDisconnected(ComponentName name) { 289 if (Log.isLoggable(TAG, Log.DEBUG)) { 290 Log.d(TAG, "onServiceDisconnected(): " + name); 291 } 292 mRadioManager = null; 293 294 // If the service has been disconnected, attempt to reconnect. 295 mHandler.removeCallbacks(mServiceConnectionRetry); 296 mHandler.postDelayed(mServiceConnectionRetry, SERVICE_CONNECTION_RETRY_DELAY_MS); 297 } 298 }; 299 300 /** 301 * A {@link Runnable} that is responsible for attempting to reconnect to {@link IRadioManager}. 302 */ 303 private Runnable mServiceConnectionRetry = new Runnable() { 304 @Override 305 public void run() { 306 if (mRadioManager != null) { 307 if (Log.isLoggable(TAG, Log.DEBUG)) { 308 Log.d(TAG, "RadioService rebound by framework, no need to bind again"); 309 } 310 return; 311 } 312 313 mConnectionRetryCount++; 314 315 if (Log.isLoggable(TAG, Log.DEBUG)) { 316 Log.d(TAG, "Rebinding disconnected RadioService, retry count: " 317 + mConnectionRetryCount); 318 } 319 320 if (!bindRadioService()) { 321 mHandler.postDelayed(mServiceConnectionRetry, 322 mConnectionRetryCount * SERVICE_CONNECTION_RETRY_DELAY_MS); 323 } 324 } 325 }; 326} 327