Add AIDL HAL support for BroadcastRadioService
Allow BroadcastRadioService using AIDL version of broadcastradio HAL. The wrapper checks whether AIDL HAL exists. If not exists, using HIDL HAL instead. Bug: 230532553 Test: atest android.hardware.radio.tests.functional Change-Id: Ia7ab0d7c3d2ee2264588a04b47ba0079dfcfe29d
This commit is contained in:
@@ -147,7 +147,8 @@ java_library_static {
|
||||
"android.hardware.boot-V1.0-java",
|
||||
"android.hardware.boot-V1.1-java",
|
||||
"android.hardware.boot-V1.2-java",
|
||||
"android.hardware.broadcastradio-V2.0-java",
|
||||
"android.hardware.broadcastradio-V2.0-java", // HIDL
|
||||
"android.hardware.broadcastradio-V1-java", // AIDL
|
||||
"android.hardware.health-V1.0-java", // HIDL
|
||||
"android.hardware.health-V2.0-java", // HIDL
|
||||
"android.hardware.health-V2.1-java", // HIDL
|
||||
|
||||
@@ -21,21 +21,23 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.radio.IRadioService;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.server.SystemService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class BroadcastRadioService extends SystemService {
|
||||
private static final String TAG = "BcRadioSrv";
|
||||
private final IRadioService mServiceImpl;
|
||||
|
||||
public BroadcastRadioService(Context context) {
|
||||
super(context);
|
||||
mServiceImpl = new BroadcastRadioServiceHidl(this);
|
||||
ArrayList<String> serviceNameList = IRadioServiceAidlImpl.getServicesNames();
|
||||
mServiceImpl = serviceNameList.isEmpty() ? new IRadioServiceHidlImpl(this)
|
||||
: new IRadioServiceAidlImpl(this, serviceNameList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
Slog.v(TAG, "BroadcastRadioService onStart()");
|
||||
publishBinderService(Context.RADIO_SERVICE, mServiceImpl.asBinder());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (C) 2022 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.server.broadcastradio;
|
||||
|
||||
import android.hardware.broadcastradio.IBroadcastRadio;
|
||||
import android.hardware.radio.IAnnouncementListener;
|
||||
import android.hardware.radio.ICloseHandle;
|
||||
import android.hardware.radio.IRadioService;
|
||||
import android.hardware.radio.ITuner;
|
||||
import android.hardware.radio.ITunerCallback;
|
||||
import android.hardware.radio.RadioManager;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.util.IndentingPrintWriter;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.server.utils.Slogf;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Wrapper for AIDL interface for BroadcastRadio HAL
|
||||
*/
|
||||
final class IRadioServiceAidlImpl extends IRadioService.Stub {
|
||||
private static final String TAG = "BcRadioSrvAidl";
|
||||
|
||||
private static final List<String> SERVICE_NAMES = Arrays.asList(
|
||||
IBroadcastRadio.DESCRIPTOR + "/amfm", IBroadcastRadio.DESCRIPTOR + "/dab");
|
||||
|
||||
private final com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl mHalAidl;
|
||||
private final BroadcastRadioService mService;
|
||||
|
||||
/**
|
||||
* Gets names of all AIDL BroadcastRadio HAL services available.
|
||||
*/
|
||||
public static ArrayList<String> getServicesNames() {
|
||||
ArrayList<String> serviceList = new ArrayList<>();
|
||||
for (int i = 0; i < SERVICE_NAMES.size(); i++) {
|
||||
IBinder serviceBinder = ServiceManager.waitForDeclaredService(SERVICE_NAMES.get(i));
|
||||
if (serviceBinder != null) {
|
||||
serviceList.add(SERVICE_NAMES.get(i));
|
||||
}
|
||||
}
|
||||
return serviceList;
|
||||
}
|
||||
|
||||
IRadioServiceAidlImpl(BroadcastRadioService service, ArrayList<String> serviceList) {
|
||||
Slogf.i(TAG, "Initialize BroadcastRadioServiceAidl(%s)", service);
|
||||
mService = Objects.requireNonNull(service);
|
||||
mHalAidl =
|
||||
new com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl(serviceList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RadioManager.ModuleProperties> listModules() {
|
||||
mService.enforcePolicyAccess();
|
||||
return mHalAidl.listModules();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig,
|
||||
boolean withAudio, ITunerCallback callback) throws RemoteException {
|
||||
if (isDebugEnabled()) {
|
||||
Slogf.d(TAG, "Opening module %d", moduleId);
|
||||
}
|
||||
mService.enforcePolicyAccess();
|
||||
if (callback == null) {
|
||||
throw new IllegalArgumentException("Callback must not be null");
|
||||
}
|
||||
return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICloseHandle addAnnouncementListener(int[] enabledTypes,
|
||||
IAnnouncementListener listener) {
|
||||
if (isDebugEnabled()) {
|
||||
Slogf.d(TAG, "Adding announcement listener for %s", Arrays.toString(enabledTypes));
|
||||
}
|
||||
Objects.requireNonNull(enabledTypes);
|
||||
Objects.requireNonNull(listener);
|
||||
mService.enforcePolicyAccess();
|
||||
|
||||
return mHalAidl.addAnnouncementListener(enabledTypes, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
|
||||
IndentingPrintWriter radioPrintWriter = new IndentingPrintWriter(printWriter);
|
||||
radioPrintWriter.printf("BroadcastRadioService\n");
|
||||
|
||||
radioPrintWriter.increaseIndent();
|
||||
radioPrintWriter.printf("AIDL HAL:\n");
|
||||
|
||||
radioPrintWriter.increaseIndent();
|
||||
mHalAidl.dumpInfo(radioPrintWriter);
|
||||
radioPrintWriter.decreaseIndent();
|
||||
|
||||
radioPrintWriter.decreaseIndent();
|
||||
}
|
||||
|
||||
private static boolean isDebugEnabled() {
|
||||
return Log.isLoggable(TAG, Log.DEBUG);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ import java.util.OptionalInt;
|
||||
/**
|
||||
* Wrapper for HIDL interface for BroadcastRadio HAL
|
||||
*/
|
||||
final class BroadcastRadioServiceHidl extends IRadioService.Stub {
|
||||
final class IRadioServiceHidlImpl extends IRadioService.Stub {
|
||||
private static final String TAG = "BcRadioSrvHidl";
|
||||
|
||||
private final com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1;
|
||||
@@ -52,7 +52,7 @@ final class BroadcastRadioServiceHidl extends IRadioService.Stub {
|
||||
private final BroadcastRadioService mService;
|
||||
private final List<RadioManager.ModuleProperties> mV1Modules;
|
||||
|
||||
BroadcastRadioServiceHidl(BroadcastRadioService service) {
|
||||
IRadioServiceHidlImpl(BroadcastRadioService service) {
|
||||
mService = Objects.requireNonNull(service);
|
||||
mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(mLock);
|
||||
mV1Modules = mHal1.loadModules();
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.server.broadcastradio.aidl;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.hardware.radio.Announcement;
|
||||
import android.hardware.radio.IAnnouncementListener;
|
||||
import android.hardware.radio.ICloseHandle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.IndentingPrintWriter;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.server.utils.Slogf;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Announcement aggregator extending {@link ICloseHandle} to support broadcast radio announcement
|
||||
*/
|
||||
public final class AnnouncementAggregator extends ICloseHandle.Stub {
|
||||
private static final String TAG = "BcRadioAidlSrv.AnnAggr";
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
private final Object mLock;
|
||||
private final IAnnouncementListener mListener;
|
||||
private final IBinder.DeathRecipient mDeathRecipient = new DeathRecipient();
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private final List<ModuleWatcher> mModuleWatchers = new ArrayList<>();
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private boolean mIsClosed;
|
||||
|
||||
/**
|
||||
* Constructs Announcement aggregator with AnnouncementListener of BroadcastRadio AIDL HAL.
|
||||
*/
|
||||
public AnnouncementAggregator(IAnnouncementListener listener, Object lock) {
|
||||
mListener = Objects.requireNonNull(listener, "listener cannot be null");
|
||||
mLock = Objects.requireNonNull(lock, "lock cannot be null");
|
||||
try {
|
||||
listener.asBinder().linkToDeath(mDeathRecipient, /* flags= */ 0);
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
private final class ModuleWatcher extends IAnnouncementListener.Stub {
|
||||
|
||||
@Nullable
|
||||
private ICloseHandle mCloseHandle;
|
||||
|
||||
public List<Announcement> mCurrentList = new ArrayList<>();
|
||||
|
||||
public void onListUpdated(List<Announcement> active) {
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "onListUpdate for %s", active);
|
||||
}
|
||||
mCurrentList = Objects.requireNonNull(active, "active cannot be null");
|
||||
AnnouncementAggregator.this.onListUpdated();
|
||||
}
|
||||
|
||||
public void setCloseHandle(ICloseHandle closeHandle) {
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "Set close handle %s", closeHandle);
|
||||
}
|
||||
mCloseHandle = Objects.requireNonNull(closeHandle, "closeHandle cannot be null");
|
||||
}
|
||||
|
||||
public void close() throws RemoteException {
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "Close module watcher.");
|
||||
}
|
||||
if (mCloseHandle != null) mCloseHandle.close();
|
||||
}
|
||||
|
||||
public void dumpInfo(IndentingPrintWriter pw) {
|
||||
pw.printf("ModuleWatcher:\n");
|
||||
|
||||
pw.increaseIndent();
|
||||
pw.printf("Close handle: %s\n", mCloseHandle);
|
||||
pw.printf("Current announcement list: %s\n", mCurrentList);
|
||||
pw.decreaseIndent();
|
||||
}
|
||||
}
|
||||
|
||||
private class DeathRecipient implements IBinder.DeathRecipient {
|
||||
public void binderDied() {
|
||||
try {
|
||||
close();
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, ex, "Cannot close Announcement aggregator for DeathRecipient");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onListUpdated() {
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "onListUpdated()");
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mIsClosed) {
|
||||
Slogf.e(TAG, "Announcement aggregator is closed, it shouldn't receive callbacks");
|
||||
return;
|
||||
}
|
||||
List<Announcement> combined = new ArrayList<>(mModuleWatchers.size());
|
||||
for (int i = 0; i < mModuleWatchers.size(); i++) {
|
||||
combined.addAll(mModuleWatchers.get(i).mCurrentList);
|
||||
}
|
||||
try {
|
||||
mListener.onListUpdated(combined);
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, ex, "mListener.onListUpdated() failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches the given RadioModule by adding Announcement Listener to it
|
||||
*/
|
||||
public void watchModule(RadioModule radioModule, int[] enabledTypes) {
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "Watch module for %s with enabled types %s",
|
||||
radioModule, Arrays.toString(enabledTypes));
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mIsClosed) {
|
||||
throw new IllegalStateException("Failed to watch module"
|
||||
+ "since announcement aggregator has already been closed");
|
||||
}
|
||||
|
||||
ModuleWatcher watcher = new ModuleWatcher();
|
||||
ICloseHandle closeHandle;
|
||||
try {
|
||||
closeHandle = radioModule.addAnnouncementListener(watcher, enabledTypes);
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, ex, "Failed to add announcement listener");
|
||||
return;
|
||||
}
|
||||
watcher.setCloseHandle(closeHandle);
|
||||
mModuleWatchers.add(watcher);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws RemoteException {
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "Close watchModule");
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mIsClosed) {
|
||||
Slogf.w(TAG, "Announcement aggregator has already been closed.");
|
||||
return;
|
||||
}
|
||||
|
||||
mListener.asBinder().unlinkToDeath(mDeathRecipient, /* flags= */ 0);
|
||||
|
||||
for (int i = 0; i < mModuleWatchers.size(); i++) {
|
||||
ModuleWatcher moduleWatcher = mModuleWatchers.get(i);
|
||||
try {
|
||||
moduleWatcher.close();
|
||||
} catch (Exception e) {
|
||||
Slogf.e(TAG, "Failed to close module watcher %s: %s",
|
||||
moduleWatcher, e);
|
||||
}
|
||||
}
|
||||
mModuleWatchers.clear();
|
||||
|
||||
mIsClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
|
||||
IndentingPrintWriter announcementPrintWriter = new IndentingPrintWriter(printWriter);
|
||||
announcementPrintWriter.printf("AnnouncementAggregator\n");
|
||||
|
||||
announcementPrintWriter.increaseIndent();
|
||||
synchronized (mLock) {
|
||||
announcementPrintWriter.printf("Is session closed? %s\n", mIsClosed ? "Yes" : "No");
|
||||
announcementPrintWriter.printf("Module Watchers [%d]:\n", mModuleWatchers.size());
|
||||
|
||||
announcementPrintWriter.increaseIndent();
|
||||
for (int i = 0; i < mModuleWatchers.size(); i++) {
|
||||
mModuleWatchers.get(i).dumpInfo(announcementPrintWriter);
|
||||
}
|
||||
announcementPrintWriter.decreaseIndent();
|
||||
|
||||
}
|
||||
announcementPrintWriter.decreaseIndent();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.server.broadcastradio.aidl;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.hardware.broadcastradio.IBroadcastRadio;
|
||||
import android.hardware.radio.IAnnouncementListener;
|
||||
import android.hardware.radio.ICloseHandle;
|
||||
import android.hardware.radio.ITuner;
|
||||
import android.hardware.radio.ITunerCallback;
|
||||
import android.hardware.radio.RadioManager;
|
||||
import android.hardware.radio.RadioTuner;
|
||||
import android.os.IBinder;
|
||||
import android.os.IServiceCallback;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.IndentingPrintWriter;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.server.utils.Slogf;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Broadcast radio service using BroadcastRadio AIDL HAL
|
||||
*/
|
||||
public final class BroadcastRadioServiceImpl {
|
||||
private static final String TAG = "BcRadioAidlSrv";
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
private final Object mLock = new Object();
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private int mNextModuleId;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private final Map<String, Integer> mServiceNameToModuleIdMap = new ArrayMap<>();
|
||||
|
||||
// Map from module ID to RadioModule created by mServiceListener.onRegistration().
|
||||
@GuardedBy("mLock")
|
||||
private final SparseArray<RadioModule> mModules = new SparseArray<>();
|
||||
|
||||
private final IServiceCallback.Stub mServiceListener = new IServiceCallback.Stub() {
|
||||
@Override
|
||||
public void onRegistration(String name, final IBinder newBinder) {
|
||||
Slogf.i(TAG, "onRegistration for %s", name);
|
||||
Integer moduleId;
|
||||
synchronized (mLock) {
|
||||
// If the service has been registered before, reuse its previous module ID.
|
||||
moduleId = mServiceNameToModuleIdMap.get(name);
|
||||
boolean newService = false;
|
||||
if (moduleId == null) {
|
||||
newService = true;
|
||||
moduleId = mNextModuleId;
|
||||
}
|
||||
|
||||
RadioModule radioModule =
|
||||
RadioModule.tryLoadingModule(moduleId, name, newBinder, mLock);
|
||||
if (radioModule == null) {
|
||||
Slogf.w(TAG, "No module %s with id %d (HAL AIDL)", name, moduleId);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
radioModule.setInternalHalCallback();
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.wtf(TAG, ex, "Broadcast radio module %s with id %d (HAL AIDL) "
|
||||
+ "cannot register HAL callback", name, moduleId);
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "Loaded broadcast radio module %s with id %d (HAL AIDL)",
|
||||
name, moduleId);
|
||||
}
|
||||
RadioModule prevModule = mModules.get(moduleId);
|
||||
mModules.put(moduleId, radioModule);
|
||||
if (prevModule != null) {
|
||||
prevModule.closeSessions(RadioTuner.ERROR_HARDWARE_FAILURE);
|
||||
}
|
||||
|
||||
if (newService) {
|
||||
mServiceNameToModuleIdMap.put(name, moduleId);
|
||||
mNextModuleId++;
|
||||
}
|
||||
|
||||
try {
|
||||
BroadcastRadioDeathRecipient deathRecipient =
|
||||
new BroadcastRadioDeathRecipient(moduleId);
|
||||
radioModule.getService().asBinder().linkToDeath(deathRecipient, moduleId);
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.w(TAG, "Service has already died, so remove its entry from mModules.");
|
||||
mModules.remove(moduleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final class BroadcastRadioDeathRecipient implements IBinder.DeathRecipient {
|
||||
private final int mModuleId;
|
||||
|
||||
BroadcastRadioDeathRecipient(int moduleId) {
|
||||
mModuleId = moduleId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
Slogf.i(TAG, "ServiceDied for module id %d", mModuleId);
|
||||
synchronized (mLock) {
|
||||
RadioModule prevModule = mModules.removeReturnOld(mModuleId);
|
||||
if (prevModule != null) {
|
||||
prevModule.closeSessions(RadioTuner.ERROR_HARDWARE_FAILURE);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Integer> entry : mServiceNameToModuleIdMap.entrySet()) {
|
||||
if (entry.getValue() == mModuleId) {
|
||||
Slogf.w(TAG, "Service %s died, removed RadioModule with ID %d",
|
||||
entry.getKey(), mModuleId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs BroadcastRadioServiceImpl using AIDL HAL using the list of names of AIDL
|
||||
* BroadcastRadio HAL services {@code serviceNameList}
|
||||
*/
|
||||
public BroadcastRadioServiceImpl(ArrayList<String> serviceNameList) {
|
||||
mNextModuleId = 0;
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "Initializing BroadcastRadioServiceImpl %s",
|
||||
IBroadcastRadio.DESCRIPTOR);
|
||||
}
|
||||
for (int i = 0; i < serviceNameList.size(); i++) {
|
||||
try {
|
||||
ServiceManager.registerForNotifications(serviceNameList.get(i), mServiceListener);
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, ex, "failed to register for service notifications for service %s",
|
||||
serviceNameList.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all AIDL {@link com.android.server.broadcastradio.aidl.RadioModule}.
|
||||
*/
|
||||
public List<RadioManager.ModuleProperties> listModules() {
|
||||
synchronized (mLock) {
|
||||
List<RadioManager.ModuleProperties> moduleList = new ArrayList<>(mModules.size());
|
||||
for (int i = 0; i < mModules.size(); i++) {
|
||||
moduleList.add(mModules.valueAt(i).mProperties);
|
||||
}
|
||||
return moduleList;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AIDL RadioModule for the given {@code moduleId}. Null will be returned if not found.
|
||||
*/
|
||||
public boolean hasModule(int id) {
|
||||
synchronized (mLock) {
|
||||
return mModules.contains(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any AIDL {@link com.android.server.broadcastradio.aidl.RadioModule} exists.
|
||||
*/
|
||||
public boolean hasAnyModules() {
|
||||
synchronized (mLock) {
|
||||
return mModules.size() != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@link ITuner} session for the AIDL
|
||||
* {@link com.android.server.broadcastradio.aidl.RadioModule} given {@code moduleId}.
|
||||
*/
|
||||
@Nullable
|
||||
public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
|
||||
boolean withAudio, ITunerCallback callback) throws RemoteException {
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "Open AIDL radio session");
|
||||
}
|
||||
Objects.requireNonNull(callback);
|
||||
|
||||
if (!withAudio) {
|
||||
throw new IllegalArgumentException("Non-audio sessions not supported with AIDL HAL");
|
||||
}
|
||||
|
||||
RadioModule radioModule;
|
||||
synchronized (mLock) {
|
||||
radioModule = mModules.get(moduleId);
|
||||
if (radioModule == null) {
|
||||
Slogf.e(TAG, "Invalid module ID %d", moduleId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
TunerSession tunerSession = radioModule.openSession(callback);
|
||||
if (legacyConfig != null) {
|
||||
tunerSession.setConfiguration(legacyConfig);
|
||||
}
|
||||
return tunerSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds AnnouncementListener for every
|
||||
* {@link com.android.server.broadcastradio.aidl.RadioModule}.
|
||||
*/
|
||||
public ICloseHandle addAnnouncementListener(int[] enabledTypes,
|
||||
IAnnouncementListener listener) {
|
||||
if (DEBUG) {
|
||||
Slogf.d(TAG, "Add AnnouncementListener with enable types %s",
|
||||
Arrays.toString(enabledTypes));
|
||||
}
|
||||
AnnouncementAggregator aggregator = new AnnouncementAggregator(listener, mLock);
|
||||
boolean anySupported = false;
|
||||
synchronized (mLock) {
|
||||
for (int i = 0; i < mModules.size(); i++) {
|
||||
try {
|
||||
aggregator.watchModule(mModules.valueAt(i), enabledTypes);
|
||||
anySupported = true;
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
Slogf.w(TAG, ex, "Announcements not supported for this module");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!anySupported) {
|
||||
Slogf.w(TAG, "There are no HAL modules that support announcements");
|
||||
}
|
||||
return aggregator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump state of broadcastradio service for AIDL HAL.
|
||||
*
|
||||
* @param pw The file to which {@link BroadcastRadioServiceImpl} state is dumped.
|
||||
*/
|
||||
public void dumpInfo(IndentingPrintWriter pw) {
|
||||
synchronized (mLock) {
|
||||
pw.printf("Next module id available: %d\n", mNextModuleId);
|
||||
pw.printf("ServiceName to module id map:\n");
|
||||
|
||||
pw.increaseIndent();
|
||||
for (Map.Entry<String, Integer> entry : mServiceNameToModuleIdMap.entrySet()) {
|
||||
pw.printf("Service name: %s, module id: %d\n", entry.getKey(), entry.getValue());
|
||||
}
|
||||
pw.decreaseIndent();
|
||||
|
||||
pw.printf("Radio modules [%d]:\n", mModules.size());
|
||||
|
||||
pw.increaseIndent();
|
||||
for (int i = 0; i < mModules.size(); i++) {
|
||||
pw.printf("Module id=%d:\n", mModules.keyAt(i));
|
||||
|
||||
pw.increaseIndent();
|
||||
mModules.valueAt(i).dumpInfo(pw);
|
||||
pw.decreaseIndent();
|
||||
}
|
||||
pw.decreaseIndent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.server.broadcastradio.aidl;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.hardware.broadcastradio.AmFmRegionConfig;
|
||||
import android.hardware.broadcastradio.Announcement;
|
||||
import android.hardware.broadcastradio.DabTableEntry;
|
||||
import android.hardware.broadcastradio.IdentifierType;
|
||||
import android.hardware.broadcastradio.Metadata;
|
||||
import android.hardware.broadcastradio.ProgramFilter;
|
||||
import android.hardware.broadcastradio.ProgramIdentifier;
|
||||
import android.hardware.broadcastradio.ProgramInfo;
|
||||
import android.hardware.broadcastradio.ProgramListChunk;
|
||||
import android.hardware.broadcastradio.Properties;
|
||||
import android.hardware.broadcastradio.Result;
|
||||
import android.hardware.broadcastradio.VendorKeyValue;
|
||||
import android.hardware.radio.ProgramList;
|
||||
import android.hardware.radio.ProgramSelector;
|
||||
import android.hardware.radio.RadioManager;
|
||||
import android.hardware.radio.RadioMetadata;
|
||||
import android.os.ParcelableException;
|
||||
import android.os.ServiceSpecificException;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.IntArray;
|
||||
|
||||
import com.android.server.utils.Slogf;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A utils class converting data types between AIDL broadcast radio HAL and
|
||||
* {@link android.hardware.radio}
|
||||
*/
|
||||
final class ConversionUtils {
|
||||
// TODO(b/241118988): Add unit test for ConversionUtils class
|
||||
private static final String TAG = "BcRadioAidlSrv.convert";
|
||||
|
||||
private ConversionUtils() {
|
||||
throw new UnsupportedOperationException("ConversionUtils class is noninstantiable");
|
||||
}
|
||||
|
||||
static RuntimeException throwOnError(RuntimeException halException, String action) {
|
||||
if (!(halException instanceof ServiceSpecificException)) {
|
||||
return new ParcelableException(new RuntimeException(
|
||||
action + ": unknown error"));
|
||||
}
|
||||
int result = ((ServiceSpecificException) halException).errorCode;
|
||||
switch (result) {
|
||||
case Result.UNKNOWN_ERROR:
|
||||
return new ParcelableException(new RuntimeException(action
|
||||
+ ": UNKNOWN_ERROR"));
|
||||
case Result.INTERNAL_ERROR:
|
||||
return new ParcelableException(new RuntimeException(action
|
||||
+ ": INTERNAL_ERROR"));
|
||||
case Result.INVALID_ARGUMENTS:
|
||||
return new IllegalArgumentException(action + ": INVALID_ARGUMENTS");
|
||||
case Result.INVALID_STATE:
|
||||
return new IllegalStateException(action + ": INVALID_STATE");
|
||||
case Result.NOT_SUPPORTED:
|
||||
return new UnsupportedOperationException(action + ": NOT_SUPPORTED");
|
||||
case Result.TIMEOUT:
|
||||
return new ParcelableException(new RuntimeException(action + ": TIMEOUT"));
|
||||
default:
|
||||
return new ParcelableException(new RuntimeException(
|
||||
action + ": unknown error (" + result + ")"));
|
||||
}
|
||||
}
|
||||
|
||||
static VendorKeyValue[] vendorInfoToHalVendorKeyValues(@Nullable Map<String, String> info) {
|
||||
if (info == null) {
|
||||
return new VendorKeyValue[]{};
|
||||
}
|
||||
|
||||
ArrayList<VendorKeyValue> list = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : info.entrySet()) {
|
||||
VendorKeyValue elem = new VendorKeyValue();
|
||||
elem.key = entry.getKey();
|
||||
elem.value = entry.getValue();
|
||||
if (elem.key == null || elem.value == null) {
|
||||
Slogf.w(TAG, "VendorKeyValue contains invalid entry: key = %s, value = %s",
|
||||
elem.key, elem.value);
|
||||
continue;
|
||||
}
|
||||
list.add(elem);
|
||||
}
|
||||
|
||||
return list.toArray(VendorKeyValue[]::new);
|
||||
}
|
||||
|
||||
static Map<String, String> vendorInfoFromHalVendorKeyValues(@Nullable VendorKeyValue[] info) {
|
||||
if (info == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<String, String> map = new ArrayMap<>();
|
||||
for (VendorKeyValue kvp : info) {
|
||||
if (kvp.key == null || kvp.value == null) {
|
||||
Slogf.w(TAG, "VendorKeyValue contains invalid entry: key = %s, value = %s",
|
||||
kvp.key, kvp.value);
|
||||
continue;
|
||||
}
|
||||
map.put(kvp.key, kvp.value);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
@ProgramSelector.ProgramType
|
||||
private static int identifierTypeToProgramType(
|
||||
@ProgramSelector.IdentifierType int idType) {
|
||||
switch (idType) {
|
||||
case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY:
|
||||
case ProgramSelector.IDENTIFIER_TYPE_RDS_PI:
|
||||
// TODO(b/69958423): verify AM/FM with frequency range
|
||||
return ProgramSelector.PROGRAM_TYPE_FM;
|
||||
case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT:
|
||||
// TODO(b/69958423): verify AM/FM with frequency range
|
||||
return ProgramSelector.PROGRAM_TYPE_FM_HD;
|
||||
case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
|
||||
case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
|
||||
case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
|
||||
case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
|
||||
return ProgramSelector.PROGRAM_TYPE_DAB;
|
||||
case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
|
||||
case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
|
||||
return ProgramSelector.PROGRAM_TYPE_DRMO;
|
||||
case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
|
||||
case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
|
||||
return ProgramSelector.PROGRAM_TYPE_SXM;
|
||||
}
|
||||
if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START
|
||||
&& idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) {
|
||||
return idType;
|
||||
}
|
||||
return ProgramSelector.PROGRAM_TYPE_INVALID;
|
||||
}
|
||||
|
||||
private static int[] identifierTypesToProgramTypes(int[] idTypes) {
|
||||
Set<Integer> programTypes = new ArraySet<>();
|
||||
|
||||
for (int i = 0; i < idTypes.length; i++) {
|
||||
int pType = identifierTypeToProgramType(idTypes[i]);
|
||||
|
||||
if (pType == ProgramSelector.PROGRAM_TYPE_INVALID) continue;
|
||||
|
||||
programTypes.add(pType);
|
||||
if (pType == ProgramSelector.PROGRAM_TYPE_FM) {
|
||||
// TODO(b/69958423): verify AM/FM with region info
|
||||
programTypes.add(ProgramSelector.PROGRAM_TYPE_AM);
|
||||
}
|
||||
if (pType == ProgramSelector.PROGRAM_TYPE_FM_HD) {
|
||||
// TODO(b/69958423): verify AM/FM with region info
|
||||
programTypes.add(ProgramSelector.PROGRAM_TYPE_AM_HD);
|
||||
}
|
||||
}
|
||||
|
||||
int[] programTypesArray = new int[programTypes.size()];
|
||||
int i = 0;
|
||||
for (int programType : programTypes) {
|
||||
programTypesArray[i++] = programType;
|
||||
}
|
||||
return programTypesArray;
|
||||
}
|
||||
|
||||
private static RadioManager.BandDescriptor[] amfmConfigToBands(
|
||||
@Nullable AmFmRegionConfig config) {
|
||||
if (config == null) {
|
||||
return new RadioManager.BandDescriptor[0];
|
||||
}
|
||||
|
||||
int len = config.ranges.length;
|
||||
List<RadioManager.BandDescriptor> bands = new ArrayList<>();
|
||||
|
||||
// Just a placeholder value.
|
||||
int region = RadioManager.REGION_ITU_1;
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
Utils.FrequencyBand bandType = Utils.getBand(config.ranges[i].lowerBound);
|
||||
if (bandType == Utils.FrequencyBand.UNKNOWN) {
|
||||
Slogf.e(TAG, "Unknown frequency band at %d kHz", config.ranges[i].lowerBound);
|
||||
continue;
|
||||
}
|
||||
if (bandType == Utils.FrequencyBand.FM) {
|
||||
bands.add(new RadioManager.FmBandDescriptor(region, RadioManager.BAND_FM,
|
||||
config.ranges[i].lowerBound, config.ranges[i].upperBound,
|
||||
config.ranges[i].spacing,
|
||||
|
||||
// TODO(b/69958777): stereo, rds, ta, af, ea
|
||||
/* stereo= */ true, /* rds= */ true, /* ta= */ true, /* af= */ true,
|
||||
/* ea= */ true
|
||||
));
|
||||
} else { // AM
|
||||
bands.add(new RadioManager.AmBandDescriptor(region, RadioManager.BAND_AM,
|
||||
config.ranges[i].lowerBound, config.ranges[i].upperBound,
|
||||
config.ranges[i].spacing,
|
||||
|
||||
// TODO(b/69958777): stereo
|
||||
/* stereo= */ true
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return bands.toArray(RadioManager.BandDescriptor[]::new);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Map<String, Integer> dabConfigFromHalDabTableEntries(
|
||||
@Nullable DabTableEntry[] config) {
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Integer> dabConfig = new ArrayMap<>();
|
||||
for (int i = 0; i < config.length; i++) {
|
||||
dabConfig.put(config[i].label, config[i].frequencyKhz);
|
||||
}
|
||||
return dabConfig;
|
||||
}
|
||||
|
||||
static RadioManager.ModuleProperties propertiesFromHalProperties(int id,
|
||||
String serviceName, Properties prop,
|
||||
@Nullable AmFmRegionConfig amfmConfig, @Nullable DabTableEntry[] dabConfig) {
|
||||
Objects.requireNonNull(serviceName);
|
||||
Objects.requireNonNull(prop);
|
||||
|
||||
int[] supportedProgramTypes = identifierTypesToProgramTypes(prop.supportedIdentifierTypes);
|
||||
|
||||
return new RadioManager.ModuleProperties(
|
||||
id,
|
||||
serviceName,
|
||||
|
||||
// There is no Class concept in HAL AIDL.
|
||||
RadioManager.CLASS_AM_FM,
|
||||
|
||||
prop.maker,
|
||||
prop.product,
|
||||
prop.version,
|
||||
prop.serial,
|
||||
|
||||
// HAL AIDL only supports single tuner and audio source per
|
||||
// HAL implementation instance.
|
||||
/* numTuners= */ 1,
|
||||
/* numAudioSources= */ 1,
|
||||
/* isInitializationRequired= */ false,
|
||||
/* isCaptureSupported= */ false,
|
||||
|
||||
amfmConfigToBands(amfmConfig),
|
||||
/* isBgScanSupported= */ true,
|
||||
supportedProgramTypes,
|
||||
prop.supportedIdentifierTypes,
|
||||
dabConfigFromHalDabTableEntries(dabConfig),
|
||||
vendorInfoFromHalVendorKeyValues(prop.vendorInfo)
|
||||
);
|
||||
}
|
||||
|
||||
static ProgramIdentifier identifierToHalProgramIdentifier(ProgramSelector.Identifier id) {
|
||||
ProgramIdentifier hwId = new ProgramIdentifier();
|
||||
hwId.type = id.getType();
|
||||
hwId.value = id.getValue();
|
||||
return hwId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static ProgramSelector.Identifier identifierFromHalProgramIdentifier(
|
||||
ProgramIdentifier id) {
|
||||
if (id.type == IdentifierType.INVALID) {
|
||||
return null;
|
||||
}
|
||||
return new ProgramSelector.Identifier(id.type, id.value);
|
||||
}
|
||||
|
||||
static android.hardware.broadcastradio.ProgramSelector programSelectorToHalProgramSelector(
|
||||
ProgramSelector sel) {
|
||||
android.hardware.broadcastradio.ProgramSelector hwSel =
|
||||
new android.hardware.broadcastradio.ProgramSelector();
|
||||
|
||||
hwSel.primaryId = identifierToHalProgramIdentifier(sel.getPrimaryId());
|
||||
ProgramSelector.Identifier[] secondaryIds = sel.getSecondaryIds();
|
||||
ArrayList<ProgramIdentifier> secondaryIdList = new ArrayList<>(secondaryIds.length);
|
||||
for (int i = 0; i < secondaryIds.length; i++) {
|
||||
secondaryIdList.add(identifierToHalProgramIdentifier(secondaryIds[i]));
|
||||
}
|
||||
hwSel.secondaryIds = secondaryIdList.toArray(ProgramIdentifier[]::new);
|
||||
return hwSel;
|
||||
}
|
||||
|
||||
private static boolean isEmpty(
|
||||
android.hardware.broadcastradio.ProgramSelector sel) {
|
||||
return sel.primaryId.type == IdentifierType.INVALID && sel.primaryId.value == 0
|
||||
&& sel.secondaryIds.length == 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static ProgramSelector programSelectorFromHalProgramSelector(
|
||||
android.hardware.broadcastradio.ProgramSelector sel) {
|
||||
if (isEmpty(sel)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ProgramSelector.Identifier> secondaryIdList = new ArrayList<>();
|
||||
for (int i = 0; i < sel.secondaryIds.length; i++) {
|
||||
if (sel.secondaryIds[i] != null) {
|
||||
secondaryIdList.add(identifierFromHalProgramIdentifier(sel.secondaryIds[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return new ProgramSelector(
|
||||
identifierTypeToProgramType(sel.primaryId.type),
|
||||
Objects.requireNonNull(identifierFromHalProgramIdentifier(sel.primaryId)),
|
||||
secondaryIdList.toArray(new ProgramSelector.Identifier[0]),
|
||||
/* vendorIds= */ null);
|
||||
}
|
||||
|
||||
private static RadioMetadata radioMetadataFromHalMetadata(Metadata[] meta) {
|
||||
RadioMetadata.Builder builder = new RadioMetadata.Builder();
|
||||
|
||||
for (int i = 0; i < meta.length; i++) {
|
||||
switch (meta[i].getTag()) {
|
||||
case Metadata.rdsPs:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_RDS_PS, meta[i].getRdsPs());
|
||||
break;
|
||||
case Metadata.rdsPty:
|
||||
builder.putInt(RadioMetadata.METADATA_KEY_RDS_PTY, meta[i].getRdsPty());
|
||||
break;
|
||||
case Metadata.rbdsPty:
|
||||
builder.putInt(RadioMetadata.METADATA_KEY_RBDS_PTY, meta[i].getRbdsPty());
|
||||
break;
|
||||
case Metadata.rdsRt:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_RDS_RT, meta[i].getRdsRt());
|
||||
break;
|
||||
case Metadata.songTitle:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_TITLE, meta[i].getSongTitle());
|
||||
break;
|
||||
case Metadata.songArtist:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_ARTIST, meta[i].getSongArtist());
|
||||
break;
|
||||
case Metadata.songAlbum:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_ALBUM, meta[i].getSongAlbum());
|
||||
break;
|
||||
case Metadata.stationIcon:
|
||||
builder.putInt(RadioMetadata.METADATA_KEY_ICON, meta[i].getStationIcon());
|
||||
break;
|
||||
case Metadata.albumArt:
|
||||
builder.putInt(RadioMetadata.METADATA_KEY_ART, meta[i].getAlbumArt());
|
||||
break;
|
||||
case Metadata.programName:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_PROGRAM_NAME,
|
||||
meta[i].getProgramName());
|
||||
break;
|
||||
case Metadata.dabEnsembleName:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME,
|
||||
meta[i].getDabEnsembleName());
|
||||
break;
|
||||
case Metadata.dabEnsembleNameShort:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT,
|
||||
meta[i].getDabEnsembleNameShort());
|
||||
break;
|
||||
case Metadata.dabServiceName:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME,
|
||||
meta[i].getDabServiceName());
|
||||
break;
|
||||
case Metadata.dabServiceNameShort:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME_SHORT,
|
||||
meta[i].getDabServiceNameShort());
|
||||
break;
|
||||
case Metadata.dabComponentName:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME,
|
||||
meta[i].getDabComponentName());
|
||||
break;
|
||||
case Metadata.dabComponentNameShort:
|
||||
builder.putString(RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME_SHORT,
|
||||
meta[i].getDabComponentNameShort());
|
||||
break;
|
||||
default:
|
||||
Slogf.w(TAG, "Ignored unknown metadata entry: %s", meta[i]);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
static RadioManager.ProgramInfo programInfoFromHalProgramInfo(ProgramInfo info) {
|
||||
Collection<ProgramSelector.Identifier> relatedContent = new ArrayList<>();
|
||||
if (info.relatedContent != null) {
|
||||
for (int i = 0; i < info.relatedContent.length; i++) {
|
||||
ProgramSelector.Identifier relatedContentId =
|
||||
identifierFromHalProgramIdentifier(info.relatedContent[i]);
|
||||
if (relatedContentId != null) {
|
||||
relatedContent.add(relatedContentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new RadioManager.ProgramInfo(
|
||||
Objects.requireNonNull(programSelectorFromHalProgramSelector(info.selector)),
|
||||
identifierFromHalProgramIdentifier(info.logicallyTunedTo),
|
||||
identifierFromHalProgramIdentifier(info.physicallyTunedTo),
|
||||
relatedContent,
|
||||
info.infoFlags,
|
||||
info.signalQuality,
|
||||
radioMetadataFromHalMetadata(info.metadata),
|
||||
vendorInfoFromHalVendorKeyValues(info.vendorInfo)
|
||||
);
|
||||
}
|
||||
|
||||
static ProgramFilter filterToHalProgramFilter(@Nullable ProgramList.Filter filter) {
|
||||
if (filter == null) {
|
||||
filter = new ProgramList.Filter();
|
||||
}
|
||||
|
||||
ProgramFilter hwFilter = new ProgramFilter();
|
||||
|
||||
IntArray identifierTypeList = new IntArray(filter.getIdentifierTypes().size());
|
||||
ArrayList<ProgramIdentifier> identifiersList = new ArrayList<>();
|
||||
Iterator<Integer> typeIterator = filter.getIdentifierTypes().iterator();
|
||||
while (typeIterator.hasNext()) {
|
||||
identifierTypeList.add(typeIterator.next());
|
||||
}
|
||||
Iterator<ProgramSelector.Identifier> idIterator = filter.getIdentifiers().iterator();
|
||||
while (idIterator.hasNext()) {
|
||||
identifiersList.add(identifierToHalProgramIdentifier(idIterator.next()));
|
||||
}
|
||||
|
||||
hwFilter.identifierTypes = identifierTypeList.toArray();
|
||||
hwFilter.identifiers = identifiersList.toArray(ProgramIdentifier[]::new);
|
||||
hwFilter.includeCategories = filter.areCategoriesIncluded();
|
||||
hwFilter.excludeModifications = filter.areModificationsExcluded();
|
||||
|
||||
return hwFilter;
|
||||
}
|
||||
|
||||
static ProgramList.Chunk chunkFromHalProgramListChunk(ProgramListChunk chunk) {
|
||||
Set<RadioManager.ProgramInfo> modified = new ArraySet<>(chunk.modified.length);
|
||||
for (int i = 0; i < chunk.modified.length; i++) {
|
||||
modified.add(programInfoFromHalProgramInfo(chunk.modified[i]));
|
||||
}
|
||||
Set<ProgramSelector.Identifier> removed = new ArraySet<>();
|
||||
if (chunk.removed != null) {
|
||||
for (int i = 0; i < chunk.removed.length; i++) {
|
||||
ProgramSelector.Identifier removedId =
|
||||
identifierFromHalProgramIdentifier(chunk.removed[i]);
|
||||
if (removedId != null) {
|
||||
removed.add(removedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed);
|
||||
}
|
||||
|
||||
public static android.hardware.radio.Announcement announcementFromHalAnnouncement(
|
||||
Announcement hwAnnouncement) {
|
||||
return new android.hardware.radio.Announcement(
|
||||
Objects.requireNonNull(programSelectorFromHalProgramSelector(
|
||||
hwAnnouncement.selector)),
|
||||
hwAnnouncement.type,
|
||||
vendorInfoFromHalVendorKeyValues(hwAnnouncement.vendorInfo)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.server.broadcastradio.aidl;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.hardware.radio.ProgramList;
|
||||
import android.hardware.radio.ProgramSelector;
|
||||
import android.hardware.radio.RadioManager;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A class to filter and update program info for HAL clients from broadcast radio AIDL HAL
|
||||
*/
|
||||
final class ProgramInfoCache {
|
||||
|
||||
/**
|
||||
* Maximum number of {@link RadioManager#ProgramInfo} elements that will be put into a
|
||||
* ProgramList.Chunk.mModified array. Used to try to ensure a single ProgramList.Chunk
|
||||
* stays within the AIDL data size limit.
|
||||
*/
|
||||
private static final int MAX_NUM_MODIFIED_PER_CHUNK = 100;
|
||||
|
||||
/**
|
||||
* Maximum number of {@link ProgramSelector#Identifier} elements that will be put
|
||||
* into the removed array of {@link ProgramList#Chunk}. Used to try to ensure a single
|
||||
* {@link ProgramList#Chunk} stays within the AIDL data size limit.
|
||||
*/
|
||||
private static final int MAX_NUM_REMOVED_PER_CHUNK = 500;
|
||||
|
||||
/**
|
||||
* Map from primary identifier to corresponding {@link RadioManager#ProgramInfo}.
|
||||
*/
|
||||
private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mProgramInfoMap =
|
||||
new ArrayMap<>();
|
||||
|
||||
/**
|
||||
* Flag indicating whether mProgramInfoMap is considered complete based upon the received
|
||||
* updates.
|
||||
*/
|
||||
private boolean mComplete = true;
|
||||
|
||||
/**
|
||||
* Optional filter used in {@link ProgramInfoCache#filterAndUpdateFromInternal}. Usually this
|
||||
* field is null for a HAL-side cache and non-null for an AIDL-side cache.
|
||||
*/
|
||||
@Nullable private final ProgramList.Filter mFilter;
|
||||
|
||||
ProgramInfoCache(@Nullable ProgramList.Filter filter) {
|
||||
mFilter = filter;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ProgramInfoCache(@Nullable ProgramList.Filter filter, boolean complete,
|
||||
RadioManager.ProgramInfo... programInfos) {
|
||||
mFilter = filter;
|
||||
mComplete = complete;
|
||||
for (int i = 0; i < programInfos.length; i++) {
|
||||
mProgramInfoMap.put(programInfos[i].getSelector().getPrimaryId(), programInfos[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean programInfosAreExactly(RadioManager.ProgramInfo... programInfos) {
|
||||
Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> expectedMap = new ArrayMap<>();
|
||||
for (int i = 0; i < programInfos.length; i++) {
|
||||
expectedMap.put(programInfos[i].getSelector().getPrimaryId(), programInfos[i]);
|
||||
}
|
||||
return expectedMap.equals(mProgramInfoMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("ProgramInfoCache(mComplete = ");
|
||||
sb.append(mComplete);
|
||||
sb.append(", mFilter = ");
|
||||
sb.append(mFilter);
|
||||
sb.append(", mProgramInfoMap = [");
|
||||
mProgramInfoMap.forEach((id, programInfo) -> {
|
||||
sb.append(", ");
|
||||
sb.append(programInfo);
|
||||
});
|
||||
return sb.append("])").toString();
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return mComplete;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ProgramList.Filter getFilter() {
|
||||
return mFilter;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void updateFromHalProgramListChunk(
|
||||
android.hardware.broadcastradio.ProgramListChunk chunk) {
|
||||
if (chunk.purge) {
|
||||
mProgramInfoMap.clear();
|
||||
}
|
||||
for (int i = 0; i < chunk.modified.length; i++) {
|
||||
RadioManager.ProgramInfo programInfo =
|
||||
ConversionUtils.programInfoFromHalProgramInfo(chunk.modified[i]);
|
||||
mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
|
||||
}
|
||||
if (chunk.removed != null) {
|
||||
for (int i = 0; i < chunk.removed.length; i++) {
|
||||
mProgramInfoMap.remove(
|
||||
ConversionUtils.identifierFromHalProgramIdentifier(chunk.removed[i]));
|
||||
}
|
||||
}
|
||||
mComplete = chunk.complete;
|
||||
}
|
||||
|
||||
List<ProgramList.Chunk> filterAndUpdateFromInternal(ProgramInfoCache other,
|
||||
boolean purge) {
|
||||
return filterAndUpdateFromInternal(other, purge, MAX_NUM_MODIFIED_PER_CHUNK,
|
||||
MAX_NUM_REMOVED_PER_CHUNK);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
List<ProgramList.Chunk> filterAndUpdateFromInternal(ProgramInfoCache other,
|
||||
boolean purge, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
|
||||
if (purge) {
|
||||
mProgramInfoMap.clear();
|
||||
}
|
||||
// If mProgramInfoMap is empty, we treat this update as a purge because this might be the
|
||||
// first update to an AIDL client that changed its filter.
|
||||
if (mProgramInfoMap.isEmpty()) {
|
||||
purge = true;
|
||||
}
|
||||
|
||||
Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
|
||||
Set<ProgramSelector.Identifier> removed = new ArraySet<>(mProgramInfoMap.keySet());
|
||||
for (Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo> entry
|
||||
: other.mProgramInfoMap.entrySet()) {
|
||||
ProgramSelector.Identifier id = entry.getKey();
|
||||
if (!passesFilter(id)) {
|
||||
continue;
|
||||
}
|
||||
removed.remove(id);
|
||||
|
||||
RadioManager.ProgramInfo newInfo = entry.getValue();
|
||||
if (!shouldIncludeInModified(newInfo)) {
|
||||
continue;
|
||||
}
|
||||
mProgramInfoMap.put(id, newInfo);
|
||||
modified.add(newInfo);
|
||||
}
|
||||
for (ProgramSelector.Identifier rem : removed) {
|
||||
mProgramInfoMap.remove(rem);
|
||||
}
|
||||
mComplete = other.mComplete;
|
||||
return buildChunks(purge, mComplete, modified, maxNumModifiedPerChunk, removed,
|
||||
maxNumRemovedPerChunk);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
List<ProgramList.Chunk> filterAndApplyChunk(ProgramList.Chunk chunk) {
|
||||
return filterAndApplyChunkInternal(chunk, MAX_NUM_MODIFIED_PER_CHUNK,
|
||||
MAX_NUM_REMOVED_PER_CHUNK);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Nullable
|
||||
List<ProgramList.Chunk> filterAndApplyChunkInternal(ProgramList.Chunk chunk,
|
||||
int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
|
||||
if (chunk.isPurge()) {
|
||||
mProgramInfoMap.clear();
|
||||
}
|
||||
|
||||
Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
|
||||
Set<ProgramSelector.Identifier> removed = new ArraySet<>();
|
||||
for (RadioManager.ProgramInfo info : chunk.getModified()) {
|
||||
ProgramSelector.Identifier id = info.getSelector().getPrimaryId();
|
||||
if (!passesFilter(id) || !shouldIncludeInModified(info)) {
|
||||
continue;
|
||||
}
|
||||
mProgramInfoMap.put(id, info);
|
||||
modified.add(info);
|
||||
}
|
||||
for (ProgramSelector.Identifier id : chunk.getRemoved()) {
|
||||
if (mProgramInfoMap.containsKey(id)) {
|
||||
mProgramInfoMap.remove(id);
|
||||
removed.add(id);
|
||||
}
|
||||
}
|
||||
if (modified.isEmpty() && removed.isEmpty() && mComplete == chunk.isComplete()
|
||||
&& !chunk.isPurge()) {
|
||||
return null;
|
||||
}
|
||||
mComplete = chunk.isComplete();
|
||||
return buildChunks(chunk.isPurge(), mComplete, modified, maxNumModifiedPerChunk, removed,
|
||||
maxNumRemovedPerChunk);
|
||||
}
|
||||
|
||||
private boolean passesFilter(ProgramSelector.Identifier id) {
|
||||
if (mFilter == null) {
|
||||
return true;
|
||||
}
|
||||
if (!mFilter.getIdentifierTypes().isEmpty()
|
||||
&& !mFilter.getIdentifierTypes().contains(id.getType())) {
|
||||
return false;
|
||||
}
|
||||
if (!mFilter.getIdentifiers().isEmpty() && !mFilter.getIdentifiers().contains(id)) {
|
||||
return false;
|
||||
}
|
||||
return mFilter.areCategoriesIncluded() || !id.isCategoryType();
|
||||
}
|
||||
|
||||
private boolean shouldIncludeInModified(RadioManager.ProgramInfo newInfo) {
|
||||
RadioManager.ProgramInfo oldInfo = mProgramInfoMap.get(
|
||||
newInfo.getSelector().getPrimaryId());
|
||||
if (oldInfo == null) {
|
||||
return true;
|
||||
}
|
||||
if (mFilter != null && mFilter.areModificationsExcluded()) {
|
||||
return false;
|
||||
}
|
||||
return !oldInfo.equals(newInfo);
|
||||
}
|
||||
|
||||
private static int roundUpFraction(int numerator, int denominator) {
|
||||
return (numerator / denominator) + (numerator % denominator > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
private static List<ProgramList.Chunk> buildChunks(boolean purge, boolean complete,
|
||||
@Nullable Collection<RadioManager.ProgramInfo> modified, int maxNumModifiedPerChunk,
|
||||
@Nullable Collection<ProgramSelector.Identifier> removed, int maxNumRemovedPerChunk) {
|
||||
// Communication protocol requires that if purge is set, removed is empty.
|
||||
if (purge) {
|
||||
removed = null;
|
||||
}
|
||||
|
||||
// Determine number of chunks we need to send.
|
||||
int numChunks = purge ? 1 : 0;
|
||||
if (modified != null) {
|
||||
numChunks = Math.max(numChunks,
|
||||
roundUpFraction(modified.size(), maxNumModifiedPerChunk));
|
||||
}
|
||||
if (removed != null) {
|
||||
numChunks = Math.max(numChunks, roundUpFraction(removed.size(), maxNumRemovedPerChunk));
|
||||
}
|
||||
if (numChunks == 0) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// Try to make similarly-sized chunks by evenly distributing elements from modified and
|
||||
// removed among them.
|
||||
int modifiedPerChunk = 0;
|
||||
int removedPerChunk = 0;
|
||||
Iterator<RadioManager.ProgramInfo> modifiedIter = null;
|
||||
Iterator<ProgramSelector.Identifier> removedIter = null;
|
||||
if (modified != null) {
|
||||
modifiedPerChunk = roundUpFraction(modified.size(), numChunks);
|
||||
modifiedIter = modified.iterator();
|
||||
}
|
||||
if (removed != null) {
|
||||
removedPerChunk = roundUpFraction(removed.size(), numChunks);
|
||||
removedIter = removed.iterator();
|
||||
}
|
||||
List<ProgramList.Chunk> chunks = new ArrayList<>(numChunks);
|
||||
for (int i = 0; i < numChunks; i++) {
|
||||
ArraySet<RadioManager.ProgramInfo> modifiedChunk = new ArraySet<>();
|
||||
ArraySet<ProgramSelector.Identifier> removedChunk = new ArraySet<>();
|
||||
if (modifiedIter != null) {
|
||||
for (int j = 0; j < modifiedPerChunk && modifiedIter.hasNext(); j++) {
|
||||
modifiedChunk.add(modifiedIter.next());
|
||||
}
|
||||
}
|
||||
if (removedIter != null) {
|
||||
for (int j = 0; j < removedPerChunk && removedIter.hasNext(); j++) {
|
||||
removedChunk.add(removedIter.next());
|
||||
}
|
||||
}
|
||||
chunks.add(new ProgramList.Chunk(purge && i == 0, complete && (i == numChunks - 1),
|
||||
modifiedChunk, removedChunk));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (C) 2022 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.server.broadcastradio.aidl;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.IndentingPrintWriter;
|
||||
import android.util.LocalLog;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.server.utils.Slogf;
|
||||
|
||||
/**
|
||||
* Event logger to log and dump events of radio module and tuner session
|
||||
* for AIDL broadcast radio HAL
|
||||
*/
|
||||
final class RadioLogger {
|
||||
private final String mTag;
|
||||
private final boolean mDebug;
|
||||
private final LocalLog mEventLogger;
|
||||
|
||||
RadioLogger(String tag, int loggerQueueSize) {
|
||||
mTag = tag;
|
||||
mDebug = Log.isLoggable(mTag, Log.DEBUG);
|
||||
mEventLogger = new LocalLog(loggerQueueSize);
|
||||
}
|
||||
|
||||
void logRadioEvent(String logFormat, Object... args) {
|
||||
String log = TextUtils.formatSimple(logFormat, args);
|
||||
mEventLogger.log(log);
|
||||
if (mDebug) {
|
||||
Slogf.d(mTag, logFormat, args);
|
||||
}
|
||||
}
|
||||
|
||||
void dump(IndentingPrintWriter pw) {
|
||||
mEventLogger.dump(pw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.server.broadcastradio.aidl;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.hardware.broadcastradio.AmFmRegionConfig;
|
||||
import android.hardware.broadcastradio.Announcement;
|
||||
import android.hardware.broadcastradio.DabTableEntry;
|
||||
import android.hardware.broadcastradio.IAnnouncementListener;
|
||||
import android.hardware.broadcastradio.IBroadcastRadio;
|
||||
import android.hardware.broadcastradio.ICloseHandle;
|
||||
import android.hardware.broadcastradio.ITunerCallback;
|
||||
import android.hardware.broadcastradio.ProgramInfo;
|
||||
import android.hardware.broadcastradio.ProgramListChunk;
|
||||
import android.hardware.broadcastradio.ProgramSelector;
|
||||
import android.hardware.broadcastradio.VendorKeyValue;
|
||||
import android.hardware.radio.RadioManager;
|
||||
import android.os.DeadObjectException;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.util.ArraySet;
|
||||
import android.util.IndentingPrintWriter;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.server.utils.Slogf;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
final class RadioModule {
|
||||
private static final String TAG = "BcRadioAidlSrv.module";
|
||||
private static final int RADIO_EVENT_LOGGER_QUEUE_SIZE = 25;
|
||||
|
||||
private final IBroadcastRadio mService;
|
||||
public final RadioManager.ModuleProperties mProperties;
|
||||
|
||||
private final Object mLock;
|
||||
private final Handler mHandler;
|
||||
private final RadioLogger mLogger;
|
||||
|
||||
/**
|
||||
* Tracks antenna state reported by HAL (if any).
|
||||
*/
|
||||
@GuardedBy("mLock")
|
||||
private Boolean mAntennaConnected;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private RadioManager.ProgramInfo mCurrentProgramInfo;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private final ProgramInfoCache mProgramInfoCache = new ProgramInfoCache(null);
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private android.hardware.radio.ProgramList.Filter mUnionOfAidlProgramFilters;
|
||||
|
||||
/**
|
||||
* Set of active AIDL tuner sessions created through openSession().
|
||||
*/
|
||||
@GuardedBy("mLock")
|
||||
private final ArraySet<TunerSession> mAidlTunerSessions = new ArraySet<>();
|
||||
|
||||
/**
|
||||
* Callback registered with the HAL to relay callbacks to AIDL clients.
|
||||
*/
|
||||
private final ITunerCallback mHalTunerCallback = new ITunerCallback.Stub() {
|
||||
@Override
|
||||
public int getInterfaceVersion() {
|
||||
return this.VERSION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInterfaceHash() {
|
||||
return this.HASH;
|
||||
}
|
||||
|
||||
public void onTuneFailed(int result, ProgramSelector programSelector) {
|
||||
fireLater(() -> {
|
||||
synchronized (mLock) {
|
||||
android.hardware.radio.ProgramSelector csel =
|
||||
ConversionUtils.programSelectorFromHalProgramSelector(programSelector);
|
||||
fanoutAidlCallbackLocked(cb -> cb.onTuneFailed(result, csel));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCurrentProgramInfoChanged(ProgramInfo halProgramInfo) {
|
||||
fireLater(() -> {
|
||||
synchronized (mLock) {
|
||||
mCurrentProgramInfo =
|
||||
ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo);
|
||||
RadioManager.ProgramInfo currentProgramInfo = mCurrentProgramInfo;
|
||||
fanoutAidlCallbackLocked(cb -> {
|
||||
cb.onCurrentProgramInfoChanged(currentProgramInfo);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgramListUpdated(ProgramListChunk programListChunk) {
|
||||
fireLater(() -> {
|
||||
synchronized (mLock) {
|
||||
android.hardware.radio.ProgramList.Chunk chunk =
|
||||
ConversionUtils.chunkFromHalProgramListChunk(programListChunk);
|
||||
mProgramInfoCache.filterAndApplyChunk(chunk);
|
||||
|
||||
for (int i = 0; i < mAidlTunerSessions.size(); i++) {
|
||||
mAidlTunerSessions.valueAt(i).onMergedProgramListUpdateFromHal(chunk);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAntennaStateChange(boolean connected) {
|
||||
fireLater(() -> {
|
||||
synchronized (mLock) {
|
||||
mAntennaConnected = connected;
|
||||
fanoutAidlCallbackLocked(cb -> cb.onAntennaState(connected));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigFlagUpdated(int flag, boolean value) {
|
||||
fireLater(() -> {
|
||||
// TODO(b/243853343): implement config flag update method in
|
||||
// android.hardware.radio.ITunerCallback
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onParametersUpdated(VendorKeyValue[] parameters) {
|
||||
fireLater(() -> {
|
||||
synchronized (mLock) {
|
||||
Map<String, String> cparam =
|
||||
ConversionUtils.vendorInfoFromHalVendorKeyValues(parameters);
|
||||
fanoutAidlCallbackLocked(cb -> cb.onParametersUpdated(cparam));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@VisibleForTesting
|
||||
RadioModule(IBroadcastRadio service,
|
||||
RadioManager.ModuleProperties properties, Object lock) {
|
||||
mProperties = Objects.requireNonNull(properties, "properties cannot be null");
|
||||
mService = Objects.requireNonNull(service, "service cannot be null");
|
||||
mLock = Objects.requireNonNull(lock, "lock cannot be null");
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mLogger = new RadioLogger(TAG, RADIO_EVENT_LOGGER_QUEUE_SIZE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static RadioModule tryLoadingModule(int moduleId, String moduleName,
|
||||
IBinder serviceBinder, Object lock) {
|
||||
try {
|
||||
Slogf.i(TAG, "Try loading module for module id = %d, module name = %s",
|
||||
moduleId, moduleName);
|
||||
IBroadcastRadio service = IBroadcastRadio.Stub
|
||||
.asInterface(serviceBinder);
|
||||
if (service == null) {
|
||||
Slogf.w(TAG, "Module %s is null", moduleName);
|
||||
return null;
|
||||
}
|
||||
|
||||
AmFmRegionConfig amfmConfig;
|
||||
try {
|
||||
amfmConfig = service.getAmFmRegionConfig(/* full= */ false);
|
||||
} catch (RuntimeException ex) {
|
||||
Slogf.i(TAG, "Module %s does not has AMFM config", moduleName);
|
||||
amfmConfig = null;
|
||||
}
|
||||
|
||||
DabTableEntry[] dabConfig;
|
||||
try {
|
||||
dabConfig = service.getDabRegionConfig();
|
||||
} catch (RuntimeException ex) {
|
||||
Slogf.i(TAG, "Module %s does not has DAB config", moduleName);
|
||||
dabConfig = null;
|
||||
}
|
||||
|
||||
RadioManager.ModuleProperties prop = ConversionUtils.propertiesFromHalProperties(
|
||||
moduleId, moduleName, service.getProperties(), amfmConfig, dabConfig);
|
||||
|
||||
return new RadioModule(service, prop, lock);
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, ex, "Failed to load module %s", moduleName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IBroadcastRadio getService() {
|
||||
return mService;
|
||||
}
|
||||
|
||||
void setInternalHalCallback() throws RemoteException {
|
||||
synchronized (mLock) {
|
||||
mService.setTunerCallback(mHalTunerCallback);
|
||||
}
|
||||
}
|
||||
|
||||
public TunerSession openSession(android.hardware.radio.ITunerCallback userCb)
|
||||
throws RemoteException {
|
||||
mLogger.logRadioEvent("Open TunerSession");
|
||||
TunerSession tunerSession;
|
||||
Boolean antennaConnected;
|
||||
RadioManager.ProgramInfo currentProgramInfo;
|
||||
synchronized (mLock) {
|
||||
tunerSession = new TunerSession(this, mService, userCb, mLock);
|
||||
mAidlTunerSessions.add(tunerSession);
|
||||
antennaConnected = mAntennaConnected;
|
||||
currentProgramInfo = mCurrentProgramInfo;
|
||||
}
|
||||
// Propagate state to new client.
|
||||
// Note: These callbacks are invoked while holding mLock to prevent race conditions
|
||||
// with new callbacks from the HAL.
|
||||
if (antennaConnected != null) {
|
||||
userCb.onAntennaState(antennaConnected);
|
||||
}
|
||||
if (currentProgramInfo != null) {
|
||||
userCb.onCurrentProgramInfoChanged(currentProgramInfo);
|
||||
}
|
||||
|
||||
return tunerSession;
|
||||
}
|
||||
|
||||
public void closeSessions(int error) {
|
||||
mLogger.logRadioEvent("Close TunerSessions %d", error);
|
||||
// TunerSession.close() must be called without mAidlTunerSessions locked because
|
||||
// it can call onTunerSessionClosed(). Therefore, the contents of mAidlTunerSessions
|
||||
// are copied into a local array here.
|
||||
TunerSession[] tunerSessions;
|
||||
synchronized (mLock) {
|
||||
tunerSessions = new TunerSession[mAidlTunerSessions.size()];
|
||||
mAidlTunerSessions.toArray(tunerSessions);
|
||||
mAidlTunerSessions.clear();
|
||||
}
|
||||
|
||||
for (TunerSession tunerSession : tunerSessions) {
|
||||
try {
|
||||
tunerSession.close(error);
|
||||
} catch (Exception e) {
|
||||
Slogf.e(TAG, "Failed to close TunerSession %s: %s", tunerSession, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
@Nullable
|
||||
private android.hardware.radio.ProgramList.Filter
|
||||
buildUnionOfTunerSessionFiltersLocked() {
|
||||
Set<Integer> idTypes = null;
|
||||
Set<android.hardware.radio.ProgramSelector.Identifier> ids = null;
|
||||
boolean includeCategories = false;
|
||||
boolean excludeModifications = true;
|
||||
|
||||
for (int i = 0; i < mAidlTunerSessions.size(); i++) {
|
||||
android.hardware.radio.ProgramList.Filter filter =
|
||||
mAidlTunerSessions.valueAt(i).getProgramListFilter();
|
||||
if (filter == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (idTypes == null) {
|
||||
idTypes = new ArraySet<>(filter.getIdentifierTypes());
|
||||
ids = new ArraySet<>(filter.getIdentifiers());
|
||||
includeCategories = filter.areCategoriesIncluded();
|
||||
excludeModifications = filter.areModificationsExcluded();
|
||||
continue;
|
||||
}
|
||||
if (!idTypes.isEmpty()) {
|
||||
if (filter.getIdentifierTypes().isEmpty()) {
|
||||
idTypes.clear();
|
||||
} else {
|
||||
idTypes.addAll(filter.getIdentifierTypes());
|
||||
}
|
||||
}
|
||||
|
||||
if (!ids.isEmpty()) {
|
||||
if (filter.getIdentifiers().isEmpty()) {
|
||||
ids.clear();
|
||||
} else {
|
||||
ids.addAll(filter.getIdentifiers());
|
||||
}
|
||||
}
|
||||
|
||||
includeCategories |= filter.areCategoriesIncluded();
|
||||
excludeModifications &= filter.areModificationsExcluded();
|
||||
}
|
||||
|
||||
return idTypes == null ? null : new android.hardware.radio.ProgramList.Filter(idTypes, ids,
|
||||
includeCategories, excludeModifications);
|
||||
}
|
||||
|
||||
void onTunerSessionProgramListFilterChanged(@Nullable TunerSession session) {
|
||||
synchronized (mLock) {
|
||||
onTunerSessionProgramListFilterChangedLocked(session);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void onTunerSessionProgramListFilterChangedLocked(@Nullable TunerSession session) {
|
||||
android.hardware.radio.ProgramList.Filter newFilter =
|
||||
buildUnionOfTunerSessionFiltersLocked();
|
||||
if (newFilter == null) {
|
||||
// If there are no AIDL clients remaining, we can stop updates from the HAL as well.
|
||||
if (mUnionOfAidlProgramFilters == null) {
|
||||
return;
|
||||
}
|
||||
mUnionOfAidlProgramFilters = null;
|
||||
try {
|
||||
mService.stopProgramListUpdates();
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, ex, "mHalTunerSession.stopProgramListUpdates() failed");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (mLock) {
|
||||
// If the HAL filter doesn't change, we can immediately send an update to the AIDL
|
||||
// client.
|
||||
if (newFilter.equals(mUnionOfAidlProgramFilters)) {
|
||||
if (session != null) {
|
||||
session.updateProgramInfoFromHalCache(mProgramInfoCache);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, update the HAL's filter, and AIDL clients will be updated when
|
||||
// mHalTunerCallback.onProgramListUpdated() is called.
|
||||
mUnionOfAidlProgramFilters = newFilter;
|
||||
try {
|
||||
mService.startProgramListUpdates(
|
||||
ConversionUtils.filterToHalProgramFilter(newFilter));
|
||||
} catch (RuntimeException ex) {
|
||||
throw ConversionUtils.throwOnError(ex, /* action= */ "Start Program ListUpdates");
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, ex, "mHalTunerSession.startProgramListUpdates() failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onTunerSessionClosed(TunerSession tunerSession) {
|
||||
synchronized (mLock) {
|
||||
onTunerSessionsClosedLocked(tunerSession);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void onTunerSessionsClosedLocked(TunerSession... tunerSessions) {
|
||||
for (TunerSession tunerSession : tunerSessions) {
|
||||
mAidlTunerSessions.remove(tunerSession);
|
||||
}
|
||||
onTunerSessionProgramListFilterChanged(null);
|
||||
}
|
||||
|
||||
// add to mHandler queue
|
||||
private void fireLater(Runnable r) {
|
||||
mHandler.post(() -> r.run());
|
||||
}
|
||||
|
||||
interface AidlCallbackRunnable {
|
||||
void run(android.hardware.radio.ITunerCallback callback) throws RemoteException;
|
||||
}
|
||||
|
||||
// Invokes runnable with each TunerSession currently open.
|
||||
void fanoutAidlCallback(AidlCallbackRunnable runnable) {
|
||||
fireLater(() -> {
|
||||
synchronized (mLock) {
|
||||
fanoutAidlCallbackLocked(runnable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void fanoutAidlCallbackLocked(AidlCallbackRunnable runnable) {
|
||||
List<TunerSession> deadSessions = null;
|
||||
for (int i = 0; i < mAidlTunerSessions.size(); i++) {
|
||||
try {
|
||||
runnable.run(mAidlTunerSessions.valueAt(i).mCallback);
|
||||
} catch (DeadObjectException ex) {
|
||||
// The other side died without calling close(), so just purge it from our records.
|
||||
Slogf.e(TAG, "Removing dead TunerSession");
|
||||
if (deadSessions == null) {
|
||||
deadSessions = new ArrayList<>();
|
||||
}
|
||||
deadSessions.add(mAidlTunerSessions.valueAt(i));
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, ex, "Failed to invoke ITunerCallback");
|
||||
}
|
||||
}
|
||||
if (deadSessions != null) {
|
||||
onTunerSessionsClosedLocked(deadSessions.toArray(
|
||||
new TunerSession[deadSessions.size()]));
|
||||
}
|
||||
}
|
||||
|
||||
public android.hardware.radio.ICloseHandle addAnnouncementListener(
|
||||
android.hardware.radio.IAnnouncementListener listener,
|
||||
int[] enabledTypes) throws RemoteException {
|
||||
mLogger.logRadioEvent("Add AnnouncementListener");
|
||||
byte[] enabledList = new byte[enabledTypes.length];
|
||||
for (int index = 0; index < enabledList.length; index++) {
|
||||
enabledList[index] = (byte) enabledTypes[index];
|
||||
}
|
||||
|
||||
final ICloseHandle[] hwCloseHandle = {null};
|
||||
IAnnouncementListener hwListener = new IAnnouncementListener.Stub() {
|
||||
public int getInterfaceVersion() {
|
||||
return this.VERSION;
|
||||
}
|
||||
|
||||
public String getInterfaceHash() {
|
||||
return this.HASH;
|
||||
}
|
||||
|
||||
public void onListUpdated(Announcement[] hwAnnouncements)
|
||||
throws RemoteException {
|
||||
List<android.hardware.radio.Announcement> announcements =
|
||||
new ArrayList<>(hwAnnouncements.length);
|
||||
for (int i = 0; i < hwAnnouncements.length; i++) {
|
||||
announcements.add(
|
||||
ConversionUtils.announcementFromHalAnnouncement(hwAnnouncements[i]));
|
||||
}
|
||||
listener.onListUpdated(announcements);
|
||||
}
|
||||
};
|
||||
|
||||
synchronized (mLock) {
|
||||
try {
|
||||
hwCloseHandle[0] = mService.registerAnnouncementListener(hwListener, enabledList);
|
||||
} catch (RuntimeException ex) {
|
||||
throw ConversionUtils.throwOnError(ex, /* action= */ "AnnouncementListener");
|
||||
}
|
||||
}
|
||||
|
||||
return new android.hardware.radio.ICloseHandle.Stub() {
|
||||
public void close() {
|
||||
try {
|
||||
hwCloseHandle[0].close();
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, ex, "Failed closing announcement listener");
|
||||
}
|
||||
hwCloseHandle[0] = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Bitmap getImage(int id) {
|
||||
mLogger.logRadioEvent("Get image for id = %d", id);
|
||||
if (id == 0) throw new IllegalArgumentException("Image ID is missing");
|
||||
|
||||
byte[] rawImage;
|
||||
synchronized (mLock) {
|
||||
try {
|
||||
rawImage = mService.getImage(id);
|
||||
} catch (RemoteException ex) {
|
||||
throw ex.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
if (rawImage == null || rawImage.length == 0) return null;
|
||||
|
||||
return BitmapFactory.decodeByteArray(rawImage, 0, rawImage.length);
|
||||
}
|
||||
|
||||
void dumpInfo(IndentingPrintWriter pw) {
|
||||
pw.printf("RadioModule\n");
|
||||
|
||||
pw.increaseIndent();
|
||||
synchronized (mLock) {
|
||||
pw.printf("BroadcastRadioServiceImpl: %s\n", mService);
|
||||
pw.printf("Properties: %s\n", mProperties);
|
||||
pw.printf("Antenna state: ");
|
||||
if (mAntennaConnected == null) {
|
||||
pw.printf("undetermined\n");
|
||||
} else {
|
||||
pw.printf("%s\n", mAntennaConnected ? "connected" : "not connected");
|
||||
}
|
||||
pw.printf("current ProgramInfo: %s\n", mCurrentProgramInfo);
|
||||
pw.printf("ProgramInfoCache: %s\n", mProgramInfoCache);
|
||||
pw.printf("Union of AIDL ProgramFilters: %s\n", mUnionOfAidlProgramFilters);
|
||||
pw.printf("AIDL TunerSessions [%d]:\n", mAidlTunerSessions.size());
|
||||
|
||||
pw.increaseIndent();
|
||||
for (int i = 0; i < mAidlTunerSessions.size(); i++) {
|
||||
mAidlTunerSessions.valueAt(i).dumpInfo(pw);
|
||||
}
|
||||
pw.decreaseIndent();
|
||||
}
|
||||
pw.printf("Radio module events:\n");
|
||||
|
||||
pw.increaseIndent();
|
||||
mLogger.dump(pw);
|
||||
pw.decreaseIndent();
|
||||
|
||||
pw.decreaseIndent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.server.broadcastradio.aidl;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.graphics.Bitmap;
|
||||
import android.hardware.broadcastradio.ConfigFlag;
|
||||
import android.hardware.broadcastradio.IBroadcastRadio;
|
||||
import android.hardware.radio.ITuner;
|
||||
import android.hardware.radio.ITunerCallback;
|
||||
import android.hardware.radio.ProgramList;
|
||||
import android.hardware.radio.ProgramSelector;
|
||||
import android.hardware.radio.RadioManager;
|
||||
import android.os.RemoteException;
|
||||
import android.util.ArraySet;
|
||||
import android.util.IndentingPrintWriter;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.server.utils.Slogf;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class TunerSession extends ITuner.Stub {
|
||||
private static final String TAG = "BcRadioAidlSrv.session";
|
||||
private static final int TUNER_EVENT_LOGGER_QUEUE_SIZE = 25;
|
||||
|
||||
private final Object mLock;
|
||||
|
||||
private final RadioLogger mLogger;
|
||||
private final RadioModule mModule;
|
||||
final android.hardware.radio.ITunerCallback mCallback;
|
||||
private final IBroadcastRadio mService;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private boolean mIsClosed;
|
||||
@GuardedBy("mLock")
|
||||
private boolean mIsMuted;
|
||||
@GuardedBy("mLock")
|
||||
private ProgramInfoCache mProgramInfoCache;
|
||||
|
||||
// necessary only for older APIs compatibility
|
||||
@GuardedBy("mLock")
|
||||
private RadioManager.BandConfig mPlaceHolderConfig;
|
||||
|
||||
TunerSession(RadioModule radioModule, IBroadcastRadio service,
|
||||
android.hardware.radio.ITunerCallback callback,
|
||||
Object lock) {
|
||||
mModule = Objects.requireNonNull(radioModule, "radioModule cannot be null");
|
||||
mService = Objects.requireNonNull(service, "service cannot be null");
|
||||
mCallback = Objects.requireNonNull(callback, "callback cannot be null");
|
||||
mLock = Objects.requireNonNull(lock, "lock cannot be null");
|
||||
mLogger = new RadioLogger(TAG, TUNER_EVENT_LOGGER_QUEUE_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mLogger.logRadioEvent("Close tuner session");
|
||||
close(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the TunerSession. If error is non-null, the client's onError() callback is invoked
|
||||
* first with the specified error, see {@link
|
||||
* android.hardware.radio.RadioTuner.Callback#onError}.
|
||||
*
|
||||
* @param error Error to send to client before session is closed. If null, there is no error
|
||||
* when closing the session.
|
||||
*/
|
||||
public void close(@Nullable Integer error) {
|
||||
if (error == null) {
|
||||
mLogger.logRadioEvent("Close tuner session on error null");
|
||||
} else {
|
||||
mLogger.logRadioEvent("Close tuner session on error %d", error);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mIsClosed) return;
|
||||
if (error != null) {
|
||||
try {
|
||||
mCallback.onError(error);
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.w(TAG, ex, "mCallback.onError(%s) failed", error);
|
||||
}
|
||||
}
|
||||
mIsClosed = true;
|
||||
mModule.onTunerSessionClosed(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
synchronized (mLock) {
|
||||
return mIsClosed;
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void checkNotClosedLocked() {
|
||||
if (mIsClosed) {
|
||||
throw new IllegalStateException("Tuner is closed, no further operations are allowed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfiguration(RadioManager.BandConfig config) {
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
mPlaceHolderConfig = Objects.requireNonNull(config, "config cannot be null");
|
||||
}
|
||||
Slogf.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL AIDL");
|
||||
mModule.fanoutAidlCallback(cb -> cb.onConfigurationChanged(config));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RadioManager.BandConfig getConfiguration() {
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
return mPlaceHolderConfig;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMuted(boolean mute) {
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
if (mIsMuted == mute) return;
|
||||
mIsMuted = mute;
|
||||
}
|
||||
Slogf.w(TAG, "Mute %b via RadioService is not implemented - please handle it via app",
|
||||
mute);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMuted() {
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
return mIsMuted;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void step(boolean directionDown, boolean skipSubChannel) throws RemoteException {
|
||||
mLogger.logRadioEvent("Step with direction %s, skipSubChannel? %s",
|
||||
directionDown ? "down" : "up", skipSubChannel ? "yes" : "no");
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
try {
|
||||
mService.step(!directionDown);
|
||||
} catch (RuntimeException ex) {
|
||||
throw ConversionUtils.throwOnError(ex, /* action= */ "step");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scan(boolean directionDown, boolean skipSubChannel) throws RemoteException {
|
||||
mLogger.logRadioEvent("Scan with direction %s, skipSubChannel? %s",
|
||||
directionDown ? "down" : "up", skipSubChannel ? "yes" : "no");
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
try {
|
||||
mService.seek(!directionDown, skipSubChannel);
|
||||
} catch (RuntimeException ex) {
|
||||
throw ConversionUtils.throwOnError(ex, /* action= */ "seek");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tune(ProgramSelector selector) throws RemoteException {
|
||||
mLogger.logRadioEvent("Tune with selector %s", selector);
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
try {
|
||||
mService.tune(ConversionUtils.programSelectorToHalProgramSelector(selector));
|
||||
} catch (RuntimeException ex) {
|
||||
throw ConversionUtils.throwOnError(ex, /* action= */ "tune");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
Slogf.i(TAG, "Cancel");
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
try {
|
||||
mService.cancel();
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.e(TAG, "Failed to cancel tuner session");
|
||||
throw ex.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAnnouncement() {
|
||||
// TODO(b/244485175): deperacte cancelAnnouncement
|
||||
Slogf.i(TAG, "Announcements control doesn't involve cancelling at the HAL level in AIDL");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap getImage(int id) {
|
||||
mLogger.logRadioEvent("Get image for %d", id);
|
||||
return mModule.getImage(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startBackgroundScan() {
|
||||
Slogf.i(TAG, "Explicit background scan trigger is not supported with HAL AIDL");
|
||||
mModule.fanoutAidlCallback(ITunerCallback::onBackgroundScanComplete);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startProgramListUpdates(ProgramList.Filter filter) throws RemoteException {
|
||||
mLogger.logRadioEvent("Start programList updates %s", filter);
|
||||
// If the AIDL client provides a null filter, it wants all updates, so use the most broad
|
||||
// filter.
|
||||
if (filter == null) {
|
||||
filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
|
||||
/* includeCategories= */ true,
|
||||
/* excludeModifications= */ false);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
mProgramInfoCache = new ProgramInfoCache(filter);
|
||||
}
|
||||
// Note: RadioModule.onTunerSessionProgramListFilterChanged() must be called without mLock
|
||||
// held since it can call getProgramListFilter() and onHalProgramInfoUpdated().
|
||||
mModule.onTunerSessionProgramListFilterChanged(this);
|
||||
}
|
||||
|
||||
ProgramList.Filter getProgramListFilter() {
|
||||
synchronized (mLock) {
|
||||
return mProgramInfoCache == null ? null : mProgramInfoCache.getFilter();
|
||||
}
|
||||
}
|
||||
|
||||
void onMergedProgramListUpdateFromHal(ProgramList.Chunk mergedChunk) {
|
||||
List<ProgramList.Chunk> clientUpdateChunks;
|
||||
synchronized (mLock) {
|
||||
if (mProgramInfoCache == null) {
|
||||
return;
|
||||
}
|
||||
clientUpdateChunks = mProgramInfoCache.filterAndApplyChunk(mergedChunk);
|
||||
}
|
||||
dispatchClientUpdateChunks(clientUpdateChunks);
|
||||
}
|
||||
|
||||
void updateProgramInfoFromHalCache(ProgramInfoCache halCache) {
|
||||
List<ProgramList.Chunk> clientUpdateChunks;
|
||||
synchronized (mLock) {
|
||||
if (mProgramInfoCache == null) {
|
||||
return;
|
||||
}
|
||||
clientUpdateChunks = mProgramInfoCache.filterAndUpdateFromInternal(
|
||||
halCache, /* purge = */ true);
|
||||
}
|
||||
dispatchClientUpdateChunks(clientUpdateChunks);
|
||||
}
|
||||
|
||||
private void dispatchClientUpdateChunks(@Nullable List<ProgramList.Chunk> chunks) {
|
||||
if (chunks == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
try {
|
||||
mCallback.onProgramListUpdated(chunks.get(i));
|
||||
} catch (RemoteException ex) {
|
||||
Slogf.w(TAG, ex, "mCallback.onProgramListUpdated() failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopProgramListUpdates() throws RemoteException {
|
||||
mLogger.logRadioEvent("Stop programList updates");
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
mProgramInfoCache = null;
|
||||
}
|
||||
// Note: RadioModule.onTunerSessionProgramListFilterChanged() must be called without mLock
|
||||
// held since it can call getProgramListFilter() and onHalProgramInfoUpdated().
|
||||
mModule.onTunerSessionProgramListFilterChanged(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigFlagSupported(int flag) {
|
||||
try {
|
||||
isConfigFlagSet(flag);
|
||||
return true;
|
||||
} catch (IllegalStateException | UnsupportedOperationException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigFlagSet(int flag) {
|
||||
mLogger.logRadioEvent("is ConfigFlag %s set? ", ConfigFlag.$.toString(flag));
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
|
||||
try {
|
||||
return mService.isConfigFlagSet(flag);
|
||||
} catch (RuntimeException ex) {
|
||||
throw ConversionUtils.throwOnError(ex, /* action= */ "isConfigFlagSet");
|
||||
} catch (RemoteException ex) {
|
||||
throw new RuntimeException("Failed to check flag " + ConfigFlag.$.toString(flag),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfigFlag(int flag, boolean value) throws RemoteException {
|
||||
mLogger.logRadioEvent("set ConfigFlag %s to %b ",
|
||||
ConfigFlag.$.toString(flag), value);
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
try {
|
||||
mService.setConfigFlag(flag, value);
|
||||
} catch (RuntimeException ex) {
|
||||
throw ConversionUtils.throwOnError(ex, /* action= */ "setConfigFlag");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> setParameters(Map<String, String> parameters) {
|
||||
mLogger.logRadioEvent("Set parameters ");
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
try {
|
||||
return ConversionUtils.vendorInfoFromHalVendorKeyValues(mService.setParameters(
|
||||
ConversionUtils.vendorInfoToHalVendorKeyValues(parameters)));
|
||||
} catch (RemoteException ex) {
|
||||
throw ex.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getParameters(List<String> keys) {
|
||||
mLogger.logRadioEvent("Get parameters ");
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
try {
|
||||
return ConversionUtils.vendorInfoFromHalVendorKeyValues(
|
||||
mService.getParameters(keys.toArray(new String[0])));
|
||||
} catch (RemoteException ex) {
|
||||
throw ex.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dumpInfo(IndentingPrintWriter pw) {
|
||||
pw.printf("TunerSession\n");
|
||||
|
||||
pw.increaseIndent();
|
||||
synchronized (mLock) {
|
||||
pw.printf("Is session closed? %s\n", mIsClosed ? "Yes" : "No");
|
||||
pw.printf("Is muted? %s\n", mIsMuted ? "Yes" : "No");
|
||||
pw.printf("ProgramInfoCache: %s\n", mProgramInfoCache);
|
||||
pw.printf("Config: %s\n", mPlaceHolderConfig);
|
||||
}
|
||||
pw.printf("Tuner session events:\n");
|
||||
|
||||
pw.increaseIndent();
|
||||
mLogger.dump(pw);
|
||||
pw.decreaseIndent();
|
||||
|
||||
pw.decreaseIndent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.server.broadcastradio.aidl;
|
||||
|
||||
final class Utils {
|
||||
|
||||
private Utils() {
|
||||
throw new UnsupportedOperationException("Utils class is noninstantiable");
|
||||
}
|
||||
|
||||
public enum FrequencyBand {
|
||||
UNKNOWN,
|
||||
FM,
|
||||
AM_LW,
|
||||
AM_MW,
|
||||
AM_SW,
|
||||
}
|
||||
|
||||
static FrequencyBand getBand(int freq) {
|
||||
// keep in sync with hardware/interfaces/broadcastradio/common/utilsaidl/Utils.cpp
|
||||
if (freq < 30) return FrequencyBand.UNKNOWN;
|
||||
if (freq < 500) return FrequencyBand.AM_LW;
|
||||
if (freq < 1705) return FrequencyBand.AM_MW;
|
||||
if (freq < 30000) return FrequencyBand.AM_SW;
|
||||
if (freq < 60000) return FrequencyBand.UNKNOWN;
|
||||
if (freq < 110000) return FrequencyBand.FM;
|
||||
return FrequencyBand.UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user