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