| /** |
| * 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<IRadioAppService> mService = new AtomicReference<>(); |
| private final Object mLock = new Object(); |
| |
| private final MutableLiveData<Integer> mConnectionState = new MutableLiveData<>(); |
| private final MutableLiveData<Integer> mPlaybackState = new MutableLiveData<>(); |
| private final MutableLiveData<ProgramInfo> mCurrentProgram = new MutableLiveData<>(); |
| private final MutableLiveData<List<ProgramInfo>> 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<ProgramInfo> 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> { |
| V execute(@NonNull IRadioAppService service) throws RemoteException; |
| } |
| |
| private <V> V queryService(@NonNull ServiceOperation<V> 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<Integer> getConnectionState() { |
| return mConnectionState; |
| } |
| |
| /** |
| * Returns a {@link LiveData} containing playback state. |
| */ |
| @NonNull |
| public LiveData<Integer> getPlaybackState() { |
| return mPlaybackState; |
| } |
| |
| /** |
| * Returns a {@link LiveData} containing currently tuned program info. |
| */ |
| @NonNull |
| public LiveData<ProgramInfo> 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<List<ProgramInfo>> 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)); |
| } |
| } |