/** * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.radio.service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager.ProgramInfo; import android.media.session.PlaybackState; import android.os.IBinder; import android.os.RemoteException; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.android.car.radio.SkipMode; import com.android.car.radio.bands.ProgramType; import com.android.car.radio.bands.RegionConfig; import com.android.car.radio.platform.RadioTunerExt.TuneCallback; import com.android.car.radio.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; /** * {@link IRadioAppService} wrapper to abstract out some nuances of interactions * with remote services. */ public class RadioAppServiceWrapper { private static final String TAG = "BcRadioApp.servicewr"; /** * Binding has just been requested and we're connecting to the {@link RadioAppService} now. */ public static final int STATE_CONNECTING = 1; /** * {@link RadioAppService} connection is up and running. */ public static final int STATE_CONNECTED = 2; /** * This device has no broadcastradio hardware. */ public static final int STATE_NOT_SUPPORTED = 3; /** * Some problem has occured (either RadioAppService crashed or there was HW problem). */ public static final int STATE_ERROR = 4; /** * Application state. */ @IntDef(value = { STATE_CONNECTING, STATE_CONNECTED, STATE_NOT_SUPPORTED, STATE_ERROR, }) @Retention(RetentionPolicy.SOURCE) public @interface ConnectionState {} private Context mClientContext; @Nullable private final AtomicReference mService = new AtomicReference<>(); private final Object mLock = new Object(); private final MutableLiveData mConnectionState = new MutableLiveData<>(); private final MutableLiveData mPlaybackState = new MutableLiveData<>(); private final MutableLiveData mCurrentProgram = new MutableLiveData<>(); private final MutableLiveData> mProgramList = new MutableLiveData<>(); { mConnectionState.postValue(STATE_CONNECTING); mPlaybackState.postValue(PlaybackState.STATE_NONE); } private static class TuneCallbackAdapter extends ITuneCallback.Stub { private final TuneCallback mCallback; private TuneCallbackAdapter(@Nullable TuneCallback cb) { mCallback = cb; } public void onFinished(boolean succeeded) { if (mCallback != null) mCallback.onFinished(succeeded); } } /** * Wraps remote service instance. * * You must call {@link #bind} once the context is available. */ public RadioAppServiceWrapper() {} /** * Wraps existing (local) service instance. * * For use by the RadioAppService itself. */ public RadioAppServiceWrapper(@NonNull IRadioAppService service) { Objects.requireNonNull(service); mService.set(service); initialize(service); } private void initialize(@NonNull IRadioAppService service) { try { service.addCallback(mCallback); } catch (RemoteException e) { throw new RuntimeException("Wrapper initialization failed", e); } } private final ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder binder) { RadioAppServiceWrapper.this.onServiceConnected(binder, Objects.requireNonNull(IRadioAppService.Stub.asInterface(binder))); } @Override public void onServiceDisconnected(ComponentName className) { onServiceFailure(); } @Override public void onBindingDied(ComponentName name) { onServiceFailure(); } @Override public void onNullBinding(ComponentName name) { RadioAppServiceWrapper.this.onNullBinding(); } }; private final IRadioAppCallback mCallback = new IRadioAppCallback.Stub() { @Override public void onHardwareError() { onServiceFailure(); } @Override public void onCurrentProgramChanged(ProgramInfo info) { mCurrentProgram.postValue(info); } @Override public void onPlaybackStateChanged(int state) { mPlaybackState.postValue(state); } @Override public void onProgramListChanged(List plist) { mProgramList.postValue(plist); } }; /** * Binds to running {@link RadioAppService} instance or starts one if it doesn't exist. */ public void bind(@NonNull Context context) { mClientContext = Objects.requireNonNull(context); Intent bindIntent = new Intent(RadioAppService.ACTION_APP_SERVICE, null, context, RadioAppService.class); if (!context.bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) { throw new RuntimeException("Failed to bind to RadioAppService"); } } /** * Unbinds from remote radio service. */ public void unbind() { if (mClientContext == null) { throw new IllegalStateException( "This is not a remote service wrapper, you can't unbind it"); } try { callService(service -> service.removeCallback(mCallback)); } catch (IllegalStateException e) { } // it's fine if the service is not connected mClientContext.unbindService(mServiceConnection); } private void onServiceConnected(IBinder binder, @NonNull IRadioAppService service) { Log.d(TAG, "RadioAppService connected"); mService.set(service); initialize(service); mConnectionState.postValue(STATE_CONNECTED); } private void onServiceFailure() { if (mService.getAndSet(null) == null) return; Log.e(TAG, "RadioAppService failed " + (mClientContext == null ? "(local)" : "(remote)")); mConnectionState.postValue(STATE_ERROR); } private void onNullBinding() { Log.i(TAG, "RadioAppService is not accepting connections. " + "It means the radio hardware is not available"); mClientContext.unbindService(mServiceConnection); mConnectionState.postValue(STATE_NOT_SUPPORTED); } private interface ServiceVoidOperation { void execute(@NonNull IRadioAppService service) throws RemoteException; } private interface ServiceOperation { V execute(@NonNull IRadioAppService service) throws RemoteException; } private V queryService(@NonNull ServiceOperation op, V defaultResponse) { IRadioAppService service = mService.get(); if (service == null) { throw new IllegalStateException("Service is not connected"); } try { return op.execute(service); } catch (RemoteException e) { Log.e(TAG, "Remote call failed", e); onServiceFailure(); return defaultResponse; } } private void callService(@NonNull ServiceVoidOperation op) { IRadioAppService service = mService.get(); if (service == null) { throw new IllegalStateException("Service is not connected"); } try { op.execute(service); } catch (RemoteException e) { Log.e(TAG, "Remote call failed", e); onServiceFailure(); } } /** * Returns a {@link LiveData} stating if the {@link RadioAppService} connection state. * * @see {@link ConnectionState}. */ @NonNull public LiveData getConnectionState() { return mConnectionState; } /** * Returns a {@link LiveData} containing playback state. */ @NonNull public LiveData getPlaybackState() { return mPlaybackState; } /** * Returns a {@link LiveData} containing currently tuned program info. */ @NonNull public LiveData getCurrentProgram() { return mCurrentProgram; } /** * Returns a {@link LiveData} containing programs list found by the background tuner. * * @return Program list container, or {@code null} if program list is not supported */ @NonNull public LiveData> getProgramList() { return mProgramList; } /** * Tunes to a given program. */ public void tune(@NonNull ProgramSelector sel) { tune(sel, null); } /** * Tunes to a given program with a callback. */ public void tune(@NonNull ProgramSelector sel, @Nullable TuneCallback result) { callService(service -> service.tune(sel, new TuneCallbackAdapter(result))); } /** * Seeks forward/backwards. */ public void seek(boolean forward) { seek(forward, null); } /** * Seeks forward/backwards with a callback. */ public void seek(boolean forward, @Nullable TuneCallback result) { callService(service -> service.seek(forward, new TuneCallbackAdapter(result))); } /** * Skips forward/backwards. */ public void skip(boolean forward) { callService(service -> service.skip(forward, new TuneCallbackAdapter(null))); } /** * Sets the service's {@link SkipMode} mode. */ public void setSkipMode(@NonNull SkipMode mode) { callService(service -> service.setSkipMode(mode.ordinal())); } /** * Steps forward/backwards */ public void step(boolean forward) { step(forward, null); } /** * Steps forward/backwards with a callback. */ public void step(boolean forward, @Nullable TuneCallback result) { callService(service -> service.step(forward, new TuneCallbackAdapter(result))); } /** * Mutes or resumes audio. * * @param muted {@code true} to mute, {@code false} to resume audio. */ public void setMuted(boolean muted) { callService(service -> service.setMuted(muted)); } /** * Tunes to the previously selected program or the default channel. */ public void tuneToDefaultIfNeeded() { callService(service -> service.tuneToDefaultIfNeeded()); } /** * Tune to a default channel of a given program type (band). * * Usually, this means tuning to the recently listened program of a given band. * * @param band Program type to switch to */ public void switchBand(@NonNull ProgramType band) { callService(service -> service.switchBand(Objects.requireNonNull(band))); } /** * States whether program list is supported on current device or not. * * @return {@code true} if the program list is supported, {@code false} otherwise. */ public boolean isProgramListSupported() { return queryService(service -> service.isProgramListSupported(), false); } /** * Returns current region config (like frequency ranges for AM/FM). */ @NonNull public RegionConfig getRegionConfig() { return Objects.requireNonNull(queryService(service -> service.getRegionConfig(), null)); } }