• 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.service;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.hardware.radio.ProgramSelector;
24 import android.hardware.radio.RadioManager.ProgramInfo;
25 import android.media.session.PlaybackState;
26 import android.os.IBinder;
27 import android.os.RemoteException;
28 
29 import androidx.annotation.IntDef;
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 import androidx.lifecycle.LiveData;
33 import androidx.lifecycle.MutableLiveData;
34 
35 import com.android.car.radio.SkipMode;
36 import com.android.car.radio.bands.ProgramType;
37 import com.android.car.radio.bands.RegionConfig;
38 import com.android.car.radio.platform.RadioTunerExt.TuneCallback;
39 import com.android.car.radio.util.Log;
40 
41 import java.lang.annotation.Retention;
42 import java.lang.annotation.RetentionPolicy;
43 import java.util.List;
44 import java.util.Objects;
45 import java.util.concurrent.atomic.AtomicReference;
46 
47 /**
48  * {@link IRadioAppService} wrapper to abstract out some nuances of interactions
49  * with remote services.
50  */
51 public class RadioAppServiceWrapper {
52     private static final String TAG = "BcRadioApp.servicewr";
53 
54     /**
55      * Binding has just been requested and we're connecting to the {@link RadioAppService} now.
56      */
57     public static final int STATE_CONNECTING = 1;
58 
59     /**
60      * {@link RadioAppService} connection is up and running.
61      */
62     public static final int STATE_CONNECTED = 2;
63 
64     /**
65      * This device has no broadcastradio hardware.
66      */
67     public static final int STATE_NOT_SUPPORTED = 3;
68 
69     /**
70      * Some problem has occured (either RadioAppService crashed or there was HW problem).
71      */
72     public static final int STATE_ERROR = 4;
73 
74     /**
75      * Application state.
76      */
77     @IntDef(value = {
78         STATE_CONNECTING,
79         STATE_CONNECTED,
80         STATE_NOT_SUPPORTED,
81         STATE_ERROR,
82     })
83     @Retention(RetentionPolicy.SOURCE)
84     public @interface ConnectionState {}
85 
86     private Context mClientContext;
87     @Nullable
88     private final AtomicReference<IRadioAppService> mService = new AtomicReference<>();
89     private final Object mLock = new Object();
90 
91     private final MutableLiveData<Integer> mConnectionState = new MutableLiveData<>();
92     private final MutableLiveData<Integer> mPlaybackState = new MutableLiveData<>();
93     private final MutableLiveData<ProgramInfo> mCurrentProgram = new MutableLiveData<>();
94     private final MutableLiveData<List<ProgramInfo>> mProgramList = new MutableLiveData<>();
95 
96     {
97         mConnectionState.postValue(STATE_CONNECTING);
98         mPlaybackState.postValue(PlaybackState.STATE_NONE);
99     }
100 
101     private static class TuneCallbackAdapter extends ITuneCallback.Stub {
102         private final TuneCallback mCallback;
103 
TuneCallbackAdapter(@ullable TuneCallback cb)104         private TuneCallbackAdapter(@Nullable TuneCallback cb) {
105             mCallback = cb;
106         }
107 
onFinished(boolean succeeded)108         public void onFinished(boolean succeeded) {
109             if (mCallback != null) mCallback.onFinished(succeeded);
110         }
111     }
112 
113     /**
114      * Wraps remote service instance.
115      *
116      * You must call {@link #bind} once the context is available.
117      */
RadioAppServiceWrapper()118     public RadioAppServiceWrapper() {}
119 
120     /**
121      * Wraps existing (local) service instance.
122      *
123      * For use by the RadioAppService itself.
124      */
RadioAppServiceWrapper(@onNull IRadioAppService service)125     public RadioAppServiceWrapper(@NonNull IRadioAppService service) {
126         Objects.requireNonNull(service);
127         mService.set(service);
128         initialize(service);
129     }
130 
initialize(@onNull IRadioAppService service)131     private void initialize(@NonNull IRadioAppService service) {
132         try {
133             service.addCallback(mCallback);
134         } catch (RemoteException e) {
135             throw new RuntimeException("Wrapper initialization failed", e);
136         }
137     }
138 
139     private final ServiceConnection mServiceConnection = new ServiceConnection() {
140         @Override
141         public void onServiceConnected(ComponentName className, IBinder binder) {
142             RadioAppServiceWrapper.this.onServiceConnected(binder,
143                     Objects.requireNonNull(IRadioAppService.Stub.asInterface(binder)));
144         }
145 
146         @Override
147         public void onServiceDisconnected(ComponentName className) {
148             onServiceFailure();
149         }
150 
151         @Override
152         public void onBindingDied(ComponentName name) {
153             onServiceFailure();
154         }
155 
156         @Override
157         public void onNullBinding(ComponentName name) {
158             RadioAppServiceWrapper.this.onNullBinding();
159         }
160     };
161 
162     private final IRadioAppCallback mCallback = new IRadioAppCallback.Stub() {
163         @Override
164         public void onHardwareError() {
165             onServiceFailure();
166         }
167 
168         @Override
169         public void onCurrentProgramChanged(ProgramInfo info) {
170             mCurrentProgram.postValue(info);
171         }
172 
173         @Override
174         public void onPlaybackStateChanged(int state) {
175             mPlaybackState.postValue(state);
176         }
177 
178         @Override
179         public void onProgramListChanged(List<ProgramInfo> plist) {
180             mProgramList.postValue(plist);
181         }
182     };
183 
184     /**
185      * Binds to running {@link RadioAppService} instance or starts one if it doesn't exist.
186      */
bind(@onNull Context context)187     public void bind(@NonNull Context context) {
188         mClientContext = Objects.requireNonNull(context);
189 
190         Intent bindIntent = new Intent(RadioAppService.ACTION_APP_SERVICE, null,
191                 context, RadioAppService.class);
192         if (!context.bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
193             throw new RuntimeException("Failed to bind to RadioAppService");
194         }
195     }
196 
197     /**
198      * Unbinds from remote radio service.
199      */
unbind()200     public void unbind() {
201         if (mClientContext == null) {
202             throw new IllegalStateException(
203                     "This is not a remote service wrapper, you can't unbind it");
204         }
205         try {
206             callService(service -> service.removeCallback(mCallback));
207         } catch (IllegalStateException e) { }  // it's fine if the service is not connected
208         mClientContext.unbindService(mServiceConnection);
209     }
210 
onServiceConnected(IBinder binder, @NonNull IRadioAppService service)211     private void onServiceConnected(IBinder binder, @NonNull IRadioAppService service) {
212         Log.d(TAG, "RadioAppService connected");
213         mService.set(service);
214         initialize(service);
215         mConnectionState.postValue(STATE_CONNECTED);
216     }
217 
onServiceFailure()218     private void onServiceFailure() {
219         if (mService.getAndSet(null) == null) return;
220         Log.e(TAG, "RadioAppService failed " + (mClientContext == null ? "(local)" : "(remote)"));
221         mConnectionState.postValue(STATE_ERROR);
222     }
223 
onNullBinding()224     private void onNullBinding() {
225         Log.i(TAG, "RadioAppService is not accepting connections. "
226                 + "It means the radio hardware is not available");
227         mClientContext.unbindService(mServiceConnection);
228         mConnectionState.postValue(STATE_NOT_SUPPORTED);
229     }
230 
231     private interface ServiceVoidOperation {
execute(@onNull IRadioAppService service)232         void execute(@NonNull IRadioAppService service) throws RemoteException;
233     }
234 
235     private interface ServiceOperation<V> {
execute(@onNull IRadioAppService service)236         V execute(@NonNull IRadioAppService service) throws RemoteException;
237     }
238 
queryService(@onNull ServiceOperation<V> op, V defaultResponse)239     private <V> V queryService(@NonNull ServiceOperation<V> op, V defaultResponse) {
240         IRadioAppService service = mService.get();
241         if (service == null) {
242             throw new IllegalStateException("Service is not connected");
243         }
244         try {
245             return op.execute(service);
246         } catch (RemoteException e) {
247             Log.e(TAG, "Remote call failed", e);
248             onServiceFailure();
249             return defaultResponse;
250         }
251     }
252 
callService(@onNull ServiceVoidOperation op)253     private void callService(@NonNull ServiceVoidOperation op) {
254         IRadioAppService service = mService.get();
255         if (service == null) {
256             throw new IllegalStateException("Service is not connected");
257         }
258         try {
259             op.execute(service);
260         } catch (RemoteException e) {
261             Log.e(TAG, "Remote call failed", e);
262             onServiceFailure();
263         }
264     }
265 
266     /**
267      * Returns a {@link LiveData} stating if the {@link RadioAppService} connection state.
268      *
269      * @see {@link ConnectionState}.
270      */
271     @NonNull
getConnectionState()272     public LiveData<Integer> getConnectionState() {
273         return mConnectionState;
274     }
275 
276     /**
277      * Returns a {@link LiveData} containing playback state.
278      */
279     @NonNull
getPlaybackState()280     public LiveData<Integer> getPlaybackState() {
281         return mPlaybackState;
282     }
283 
284     /**
285      * Returns a {@link LiveData} containing currently tuned program info.
286      */
287     @NonNull
getCurrentProgram()288     public LiveData<ProgramInfo> getCurrentProgram() {
289         return mCurrentProgram;
290     }
291 
292     /**
293      * Returns a {@link LiveData} containing programs list found by the background tuner.
294      *
295      * @return Program list container, or {@code null} if program list is not supported
296      */
297     @NonNull
getProgramList()298     public LiveData<List<ProgramInfo>> getProgramList() {
299         return mProgramList;
300     }
301 
302     /**
303      * Tunes to a given program.
304      */
tune(@onNull ProgramSelector sel)305     public void tune(@NonNull ProgramSelector sel) {
306         tune(sel, null);
307     }
308 
309     /**
310      * Tunes to a given program with a callback.
311      */
tune(@onNull ProgramSelector sel, @Nullable TuneCallback result)312     public void tune(@NonNull ProgramSelector sel, @Nullable TuneCallback result) {
313         callService(service -> service.tune(sel, new TuneCallbackAdapter(result)));
314     }
315 
316     /**
317      * Seeks forward/backwards.
318      */
seek(boolean forward)319     public void seek(boolean forward) {
320         seek(forward, null);
321     }
322 
323     /**
324      * Seeks forward/backwards with a callback.
325      */
seek(boolean forward, @Nullable TuneCallback result)326     public void seek(boolean forward, @Nullable TuneCallback result) {
327         callService(service -> service.seek(forward, new TuneCallbackAdapter(result)));
328     }
329 
330     /**
331      * Skips forward/backwards.
332      */
skip(boolean forward)333     public void skip(boolean forward) {
334         callService(service -> service.skip(forward, new TuneCallbackAdapter(null)));
335     }
336 
337     /**
338      * Sets the service's {@link SkipMode} mode.
339      */
setSkipMode(@onNull SkipMode mode)340     public void setSkipMode(@NonNull SkipMode mode) {
341         callService(service -> service.setSkipMode(mode.ordinal()));
342     }
343 
344     /**
345      * Steps forward/backwards
346      */
step(boolean forward)347     public void step(boolean forward) {
348         step(forward, null);
349     }
350 
351     /**
352      * Steps forward/backwards with a callback.
353      */
step(boolean forward, @Nullable TuneCallback result)354     public void step(boolean forward, @Nullable TuneCallback result) {
355         callService(service -> service.step(forward, new TuneCallbackAdapter(result)));
356     }
357 
358     /**
359      * Mutes or resumes audio.
360      *
361      * @param muted {@code true} to mute, {@code false} to resume audio.
362      */
setMuted(boolean muted)363     public void setMuted(boolean muted) {
364         callService(service -> service.setMuted(muted));
365     }
366 
367     /**
368      * Tune to a default channel of a given program type (band).
369      *
370      * Usually, this means tuning to the recently listened program of a given band.
371      *
372      * @param band Program type to switch to
373      */
switchBand(@onNull ProgramType band)374     public void switchBand(@NonNull ProgramType band) {
375         callService(service -> service.switchBand(Objects.requireNonNull(band)));
376     }
377 
378     /**
379      * States whether program list is supported on current device or not.
380      *
381      * @return {@code true} if the program list is supported, {@code false} otherwise.
382      */
isProgramListSupported()383     public boolean isProgramListSupported() {
384         return queryService(service -> service.isProgramListSupported(), false);
385     }
386 
387     /**
388      * Returns current region config (like frequency ranges for AM/FM).
389      */
390     @NonNull
getRegionConfig()391     public RegionConfig getRegionConfig() {
392         return Objects.requireNonNull(queryService(service -> service.getRegionConfig(), null));
393     }
394 }
395