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:
Weilin Xu
2022-06-10 20:26:42 +00:00
parent 61f5f1585d
commit 67fa6636cd
12 changed files with 2428 additions and 7 deletions

View File

@@ -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

View File

@@ -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());
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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)
);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}