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