diff --git a/services/core/Android.bp b/services/core/Android.bp index cbd3f6087df25..1742af4a60008 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -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 diff --git a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java index ab553a864ad63..3ede0a2597d91 100644 --- a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java +++ b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java @@ -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 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()); } diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java new file mode 100644 index 0000000000000..0770062cd4d3b --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java @@ -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 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 getServicesNames() { + ArrayList 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 serviceList) { + Slogf.i(TAG, "Initialize BroadcastRadioServiceAidl(%s)", service); + mService = Objects.requireNonNull(service); + mHalAidl = + new com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl(serviceList); + } + + @Override + public List 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); + } +} diff --git a/services/core/java/com/android/server/broadcastradio/BroadcastRadioServiceHidl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java similarity index 97% rename from services/core/java/com/android/server/broadcastradio/BroadcastRadioServiceHidl.java rename to services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java index 5cb6770e7909f..28b6d02581bee 100644 --- a/services/core/java/com/android/server/broadcastradio/BroadcastRadioServiceHidl.java +++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java @@ -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 mV1Modules; - BroadcastRadioServiceHidl(BroadcastRadioService service) { + IRadioServiceHidlImpl(BroadcastRadioService service) { mService = Objects.requireNonNull(service); mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(mLock); mV1Modules = mHal1.loadModules(); diff --git a/services/core/java/com/android/server/broadcastradio/aidl/AnnouncementAggregator.java b/services/core/java/com/android/server/broadcastradio/aidl/AnnouncementAggregator.java new file mode 100644 index 0000000000000..b618aa3d65dc2 --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/aidl/AnnouncementAggregator.java @@ -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 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 mCurrentList = new ArrayList<>(); + + public void onListUpdated(List 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 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(); + } + +} diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java new file mode 100644 index 0000000000000..71ba2968baec5 --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java @@ -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 mServiceNameToModuleIdMap = new ArrayMap<>(); + + // Map from module ID to RadioModule created by mServiceListener.onRegistration(). + @GuardedBy("mLock") + private final SparseArray 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 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 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 listModules() { + synchronized (mLock) { + List 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 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(); + } + } +} diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java new file mode 100644 index 0000000000000..d90f9c47f10ac --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java @@ -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 info) { + if (info == null) { + return new VendorKeyValue[]{}; + } + + ArrayList list = new ArrayList<>(); + for (Map.Entry 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 vendorInfoFromHalVendorKeyValues(@Nullable VendorKeyValue[] info) { + if (info == null) { + return Collections.emptyMap(); + } + + Map 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 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 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 dabConfigFromHalDabTableEntries( + @Nullable DabTableEntry[] config) { + if (config == null) { + return null; + } + Map 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 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 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 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 identifiersList = new ArrayList<>(); + Iterator typeIterator = filter.getIdentifierTypes().iterator(); + while (typeIterator.hasNext()) { + identifierTypeList.add(typeIterator.next()); + } + Iterator 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 modified = new ArraySet<>(chunk.modified.length); + for (int i = 0; i < chunk.modified.length; i++) { + modified.add(programInfoFromHalProgramInfo(chunk.modified[i])); + } + Set 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) + ); + } +} diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java new file mode 100644 index 0000000000000..095a5fa1cf302 --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java @@ -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 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 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 filterAndUpdateFromInternal(ProgramInfoCache other, + boolean purge) { + return filterAndUpdateFromInternal(other, purge, MAX_NUM_MODIFIED_PER_CHUNK, + MAX_NUM_REMOVED_PER_CHUNK); + } + + @VisibleForTesting + List 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 modified = new ArraySet<>(); + Set removed = new ArraySet<>(mProgramInfoMap.keySet()); + for (Map.Entry 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 filterAndApplyChunk(ProgramList.Chunk chunk) { + return filterAndApplyChunkInternal(chunk, MAX_NUM_MODIFIED_PER_CHUNK, + MAX_NUM_REMOVED_PER_CHUNK); + } + + @VisibleForTesting + @Nullable + List filterAndApplyChunkInternal(ProgramList.Chunk chunk, + int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) { + if (chunk.isPurge()) { + mProgramInfoMap.clear(); + } + + Set modified = new ArraySet<>(); + Set 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 buildChunks(boolean purge, boolean complete, + @Nullable Collection modified, int maxNumModifiedPerChunk, + @Nullable Collection 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 modifiedIter = null; + Iterator 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 chunks = new ArrayList<>(numChunks); + for (int i = 0; i < numChunks; i++) { + ArraySet modifiedChunk = new ArraySet<>(); + ArraySet 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; + } +} diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioLogger.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioLogger.java new file mode 100644 index 0000000000000..cca351bc0d73c --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioLogger.java @@ -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); + } +} diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java new file mode 100644 index 0000000000000..c6dc431976308 --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java @@ -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 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 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 idTypes = null; + Set 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 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 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(); + } +} diff --git a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java new file mode 100644 index 0000000000000..7c26a8741a03a --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java @@ -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 clientUpdateChunks; + synchronized (mLock) { + if (mProgramInfoCache == null) { + return; + } + clientUpdateChunks = mProgramInfoCache.filterAndApplyChunk(mergedChunk); + } + dispatchClientUpdateChunks(clientUpdateChunks); + } + + void updateProgramInfoFromHalCache(ProgramInfoCache halCache) { + List clientUpdateChunks; + synchronized (mLock) { + if (mProgramInfoCache == null) { + return; + } + clientUpdateChunks = mProgramInfoCache.filterAndUpdateFromInternal( + halCache, /* purge = */ true); + } + dispatchClientUpdateChunks(clientUpdateChunks); + } + + private void dispatchClientUpdateChunks(@Nullable List 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 setParameters(Map 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 getParameters(List 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(); + } +} diff --git a/services/core/java/com/android/server/broadcastradio/aidl/Utils.java b/services/core/java/com/android/server/broadcastradio/aidl/Utils.java new file mode 100644 index 0000000000000..b6bea5b18e284 --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/aidl/Utils.java @@ -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; + } + +}