blob: cfd07af56648f0d7b0f871e3f76d2da3827cee4e [file] [log] [blame]
/**
* 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));
}
}