Implement front-end APIs for announcements.

Bug: 68045105
Test: instrumentation (none added)
Change-Id: I602e8bb0c40516a732d606f745c8f7721583155f
diff --git a/Android.bp b/Android.bp
index d1332bb..c571c802 100644
--- a/Android.bp
+++ b/Android.bp
@@ -177,6 +177,8 @@
         "core/java/android/hardware/location/IContextHubClientCallback.aidl",
         "core/java/android/hardware/location/IContextHubService.aidl",
         "core/java/android/hardware/location/IContextHubTransactionCallback.aidl",
+        "core/java/android/hardware/radio/IAnnouncementListener.aidl",
+        "core/java/android/hardware/radio/ICloseHandle.aidl",
         "core/java/android/hardware/radio/IRadioService.aidl",
         "core/java/android/hardware/radio/ITuner.aidl",
         "core/java/android/hardware/radio/ITunerCallback.aidl",
diff --git a/api/system-current.txt b/api/system-current.txt
index a1ec2c4..9e5c30b 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1739,6 +1739,27 @@
 
 package android.hardware.radio {
 
+  public final class Announcement implements android.os.Parcelable {
+    method public int describeContents();
+    method public android.hardware.radio.ProgramSelector getSelector();
+    method public int getType();
+    method public java.util.Map<java.lang.String, java.lang.String> getVendorInfo();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.hardware.radio.Announcement> CREATOR;
+    field public static final int TYPE_EMERGENCY = 1; // 0x1
+    field public static final int TYPE_EVENT = 6; // 0x6
+    field public static final int TYPE_MISC = 8; // 0x8
+    field public static final int TYPE_NEWS = 5; // 0x5
+    field public static final int TYPE_SPORT = 7; // 0x7
+    field public static final int TYPE_TRAFFIC = 3; // 0x3
+    field public static final int TYPE_WARNING = 2; // 0x2
+    field public static final int TYPE_WEATHER = 4; // 0x4
+  }
+
+  public static abstract interface Announcement.OnListUpdatedListener {
+    method public abstract void onListUpdated(java.util.Collection<android.hardware.radio.Announcement>);
+  }
+
   public final class ProgramList implements java.lang.AutoCloseable {
     method public void addOnCompleteListener(java.util.concurrent.Executor, android.hardware.radio.ProgramList.OnCompleteListener);
     method public void addOnCompleteListener(android.hardware.radio.ProgramList.OnCompleteListener);
@@ -1833,8 +1854,11 @@
   }
 
   public class RadioManager {
+    method public void addAnnouncementListener(java.util.Set<java.lang.Integer>, android.hardware.radio.Announcement.OnListUpdatedListener);
+    method public void addAnnouncementListener(java.util.concurrent.Executor, java.util.Set<java.lang.Integer>, android.hardware.radio.Announcement.OnListUpdatedListener);
     method public int listModules(java.util.List<android.hardware.radio.RadioManager.ModuleProperties>);
     method public android.hardware.radio.RadioTuner openTuner(int, android.hardware.radio.RadioManager.BandConfig, boolean, android.hardware.radio.RadioTuner.Callback, android.os.Handler);
+    method public void removeAnnouncementListener(android.hardware.radio.Announcement.OnListUpdatedListener);
     field public static final int BAND_AM = 0; // 0x0
     field public static final int BAND_AM_HD = 3; // 0x3
     field public static final int BAND_FM = 1; // 0x1
diff --git a/core/java/android/hardware/radio/Announcement.aidl b/core/java/android/hardware/radio/Announcement.aidl
new file mode 100644
index 0000000..eeb5951
--- /dev/null
+++ b/core/java/android/hardware/radio/Announcement.aidl
@@ -0,0 +1,20 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+/** @hide */
+parcelable Announcement;
diff --git a/core/java/android/hardware/radio/Announcement.java b/core/java/android/hardware/radio/Announcement.java
new file mode 100644
index 0000000..166fe60
--- /dev/null
+++ b/core/java/android/hardware/radio/Announcement.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+@SystemApi
+public final class Announcement implements Parcelable {
+
+    /** DAB alarm, RDS emergency program type (PTY 31). */
+    public static final int TYPE_EMERGENCY = 1;
+    /** DAB warning. */
+    public static final int TYPE_WARNING = 2;
+    /** DAB road traffic, RDS TA, HD Radio transportation. */
+    public static final int TYPE_TRAFFIC = 3;
+    /** Weather. */
+    public static final int TYPE_WEATHER = 4;
+    /** News. */
+    public static final int TYPE_NEWS = 5;
+    /** DAB event, special event. */
+    public static final int TYPE_EVENT = 6;
+    /** DAB sport report, RDS sports. */
+    public static final int TYPE_SPORT = 7;
+    /** All others. */
+    public static final int TYPE_MISC = 8;
+    /** @hide */
+    @IntDef(prefix = { "TYPE_" }, value = {
+        TYPE_EMERGENCY,
+        TYPE_WARNING,
+        TYPE_TRAFFIC,
+        TYPE_WEATHER,
+        TYPE_NEWS,
+        TYPE_EVENT,
+        TYPE_SPORT,
+        TYPE_MISC,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    /**
+     * Listener of announcement list events.
+     */
+    public interface OnListUpdatedListener {
+        /**
+         * An event called whenever a list of active announcements change.
+         *
+         * The entire list is sent each time a new announcement appears or any ends broadcasting.
+         *
+         * @param activeAnnouncements a full list of active announcements
+         */
+        void onListUpdated(Collection<Announcement> activeAnnouncements);
+    }
+
+    @NonNull private final ProgramSelector mSelector;
+    @Type private final int mType;
+    @NonNull private final Map<String, String> mVendorInfo;
+
+    /** @hide */
+    public Announcement(@NonNull ProgramSelector selector, @Type int type,
+            @NonNull Map<String, String> vendorInfo) {
+        mSelector = Objects.requireNonNull(selector);
+        mType = Objects.requireNonNull(type);
+        mVendorInfo = Objects.requireNonNull(vendorInfo);
+    }
+
+    private Announcement(@NonNull Parcel in) {
+        mSelector = in.readTypedObject(ProgramSelector.CREATOR);
+        mType = in.readInt();
+        mVendorInfo = Utils.readStringMap(in);
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeTypedObject(mSelector, 0);
+        dest.writeInt(mType);
+        Utils.writeStringMap(dest, mVendorInfo);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<Announcement> CREATOR =
+            new Parcelable.Creator<Announcement>() {
+        public Announcement createFromParcel(Parcel in) {
+            return new Announcement(in);
+        }
+
+        public Announcement[] newArray(int size) {
+            return new Announcement[size];
+        }
+    };
+
+    public @NonNull ProgramSelector getSelector() {
+        return mSelector;
+    }
+
+    public @Type int getType() {
+        return mType;
+    }
+
+    public @NonNull Map<String, String> getVendorInfo() {
+        return mVendorInfo;
+    }
+}
diff --git a/core/java/android/hardware/radio/IAnnouncementListener.aidl b/core/java/android/hardware/radio/IAnnouncementListener.aidl
new file mode 100644
index 0000000..b4d974a
--- /dev/null
+++ b/core/java/android/hardware/radio/IAnnouncementListener.aidl
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+import android.hardware.radio.Announcement;
+
+/** {@hide} */
+oneway interface IAnnouncementListener {
+    void onListUpdated(in List<Announcement> activeAnnouncements);
+}
diff --git a/core/java/android/hardware/radio/ICloseHandle.aidl b/core/java/android/hardware/radio/ICloseHandle.aidl
new file mode 100644
index 0000000..576c03b
--- /dev/null
+++ b/core/java/android/hardware/radio/ICloseHandle.aidl
@@ -0,0 +1,22 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+/** {@hide} */
+interface ICloseHandle {
+    void close();
+}
diff --git a/core/java/android/hardware/radio/IRadioService.aidl b/core/java/android/hardware/radio/IRadioService.aidl
index c43fd26..9349cf7 100644
--- a/core/java/android/hardware/radio/IRadioService.aidl
+++ b/core/java/android/hardware/radio/IRadioService.aidl
@@ -16,6 +16,8 @@
 
 package android.hardware.radio;
 
+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;
@@ -30,4 +32,7 @@
 
     ITuner openTuner(int moduleId, in RadioManager.BandConfig bandConfig, boolean withAudio,
             in ITunerCallback callback);
+
+    ICloseHandle addAnnouncementListener(in int[] enabledTypes,
+            in IAnnouncementListener listener);
 }
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index 56668ac..c219759 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -17,6 +17,7 @@
 package android.hardware.radio;
 
 import android.Manifest;
+import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
@@ -40,6 +41,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
 /**
@@ -1713,6 +1715,68 @@
                 config != null ? config.getType() : BAND_INVALID);
     }
 
+    private final Map<Announcement.OnListUpdatedListener, ICloseHandle> mAnnouncementListeners =
+            new HashMap<>();
+
+    /**
+     * Adds new announcement listener.
+     *
+     * @param enabledAnnouncementTypes a set of announcement types to listen to
+     * @param listener announcement listener
+     */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    public void addAnnouncementListener(@NonNull Set<Integer> enabledAnnouncementTypes,
+            @NonNull Announcement.OnListUpdatedListener listener) {
+        addAnnouncementListener(cmd -> cmd.run(), enabledAnnouncementTypes, listener);
+    }
+
+    /**
+     * Adds new announcement listener with executor.
+     *
+     * @param executor the executor
+     * @param enabledAnnouncementTypes a set of announcement types to listen to
+     * @param listener announcement listener
+     */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    public void addAnnouncementListener(@NonNull @CallbackExecutor Executor executor,
+            @NonNull Set<Integer> enabledAnnouncementTypes,
+            @NonNull Announcement.OnListUpdatedListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        int[] types = enabledAnnouncementTypes.stream().mapToInt(Integer::intValue).toArray();
+        IAnnouncementListener listenerIface = new IAnnouncementListener.Stub() {
+            public void onListUpdated(List<Announcement> activeAnnouncements) {
+                executor.execute(() -> listener.onListUpdated(activeAnnouncements));
+            }
+        };
+        synchronized (mAnnouncementListeners) {
+            ICloseHandle closeHandle = null;
+            try {
+                closeHandle = mService.addAnnouncementListener(types, listenerIface);
+            } catch (RemoteException ex) {
+                ex.rethrowFromSystemServer();
+            }
+            Objects.requireNonNull(closeHandle);
+            ICloseHandle oldCloseHandle = mAnnouncementListeners.put(listener, closeHandle);
+            if (oldCloseHandle != null) Utils.close(oldCloseHandle);
+        }
+    }
+
+    /**
+     * Removes previously registered announcement listener.
+     *
+     * @param listener announcement listener, previously registered with
+     *        {@link addAnnouncementListener}
+     */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    public void removeAnnouncementListener(@NonNull Announcement.OnListUpdatedListener listener) {
+        Objects.requireNonNull(listener);
+        synchronized (mAnnouncementListeners) {
+            ICloseHandle closeHandle = mAnnouncementListeners.remove(listener);
+            if (closeHandle != null) Utils.close(closeHandle);
+        }
+    }
+
     @NonNull private final Context mContext;
     @NonNull private final IRadioService mService;
 
diff --git a/core/java/android/hardware/radio/Utils.java b/core/java/android/hardware/radio/Utils.java
index 09bf8fe..a80bbf2 100644
--- a/core/java/android/hardware/radio/Utils.java
+++ b/core/java/android/hardware/radio/Utils.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.RemoteException;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -28,6 +29,8 @@
 import java.util.Set;
 
 final class Utils {
+    private static final String TAG = "BroadcastRadio.utils";
+
     static void writeStringMap(@NonNull Parcel dest, @Nullable Map<String, String> map) {
         if (map == null) {
             dest.writeInt(0);
@@ -89,4 +92,12 @@
             }
         });
     }
+
+    static void close(ICloseHandle handle) {
+        try {
+            handle.close();
+        } catch (RemoteException ex) {
+            ex.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
index 10e6cad..4289a25 100644
--- a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
@@ -20,6 +20,8 @@
 import android.Manifest;
 import android.content.Context;
 import android.content.pm.PackageManager;
+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;
@@ -28,14 +30,18 @@
 import android.os.RemoteException;
 import android.util.Slog;
 
+import com.android.internal.util.Preconditions;
 import com.android.server.SystemService;
+import com.android.server.broadcastradio.hal2.AnnouncementAggregator;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.OptionalInt;
 
 public class BroadcastRadioService extends SystemService {
     private static final String TAG = "BcRadioSrv";
+    private static final boolean DEBUG = false;
 
     private final ServiceImpl mServiceImpl = new ServiceImpl();
 
@@ -88,7 +94,7 @@
         @Override
         public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig,
                 boolean withAudio, ITunerCallback callback) throws RemoteException {
-            Slog.i(TAG, "openTuner(" + moduleId + ", _, " + withAudio + ", _)");
+            if (DEBUG) Slog.i(TAG, "Opening module " + moduleId);
             enforcePolicyAccess();
             if (callback == null) {
                 throw new IllegalArgumentException("Callback must not be empty");
@@ -101,5 +107,25 @@
                 }
             }
         }
+
+        @Override
+        public ICloseHandle addAnnouncementListener(int[] enabledTypes,
+                IAnnouncementListener listener) {
+            if (DEBUG) {
+                Slog.i(TAG, "Adding announcement listener for " + Arrays.toString(enabledTypes));
+            }
+            Objects.requireNonNull(enabledTypes);
+            Objects.requireNonNull(listener);
+            enforcePolicyAccess();
+
+            synchronized (mLock) {
+                if (!mHal2.hasAnyModules()) {
+                    Slog.i(TAG, "There are no HAL 2.x modules registered");
+                    return new AnnouncementAggregator(listener);
+                }
+
+                return mHal2.addAnnouncementListener(enabledTypes, listener);
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
new file mode 100644
index 0000000..0bbaf25
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.broadcastradio.hal2;
+
+import android.annotation.NonNull;
+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.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+public class AnnouncementAggregator extends ICloseHandle.Stub {
+    private static final String TAG = "BcRadio2Srv.AnnAggr";
+
+    private final Object mLock = new Object();
+    @NonNull private final IAnnouncementListener mListener;
+    private final IBinder.DeathRecipient mDeathRecipient = new DeathRecipient();
+
+    @GuardedBy("mLock")
+    private final Collection<ModuleWatcher> mModuleWatchers = new ArrayList<>();
+
+    @GuardedBy("mLock")
+    private boolean mIsClosed = false;
+
+    public AnnouncementAggregator(@NonNull IAnnouncementListener listener) {
+        mListener = Objects.requireNonNull(listener);
+        try {
+            listener.asBinder().linkToDeath(mDeathRecipient, 0);
+        } catch (RemoteException ex) {
+            ex.rethrowFromSystemServer();
+        }
+    }
+
+    private class ModuleWatcher extends IAnnouncementListener.Stub {
+        private @Nullable ICloseHandle mCloseHandle;
+        public @NonNull List<Announcement> currentList = new ArrayList<>();
+
+        public void onListUpdated(List<Announcement> active) {
+            currentList = Objects.requireNonNull(active);
+            AnnouncementAggregator.this.onListUpdated();
+        }
+
+        public void setCloseHandle(@NonNull ICloseHandle closeHandle) {
+            mCloseHandle = Objects.requireNonNull(closeHandle);
+        }
+
+        public void close() throws RemoteException {
+            if (mCloseHandle != null) mCloseHandle.close();
+        }
+    }
+
+    private class DeathRecipient implements IBinder.DeathRecipient {
+        public void binderDied() {
+            try {
+                close();
+            } catch (RemoteException ex) {}
+        }
+    }
+
+    private void onListUpdated() {
+        synchronized (mLock) {
+            if (mIsClosed) {
+                Slog.e(TAG, "Announcement aggregator is closed, it shouldn't receive callbacks");
+                return;
+            }
+            List<Announcement> combined = new ArrayList<>();
+            for (ModuleWatcher watcher : mModuleWatchers) {
+                combined.addAll(watcher.currentList);
+            }
+            TunerCallback.dispatch(() -> mListener.onListUpdated(combined));
+        }
+    }
+
+    public void watchModule(@NonNull RadioModule module, @NonNull int[] enabledTypes) {
+        synchronized (mLock) {
+            if (mIsClosed) throw new IllegalStateException();
+
+            ModuleWatcher watcher = new ModuleWatcher();
+            ICloseHandle closeHandle;
+            try {
+                closeHandle = module.addAnnouncementListener(enabledTypes, watcher);
+            } catch (RemoteException ex) {
+                Slog.e(TAG, "Failed to add announcement listener", ex);
+                return;
+            }
+            watcher.setCloseHandle(closeHandle);
+            mModuleWatchers.add(watcher);
+        }
+    }
+
+    @Override
+    public void close() throws RemoteException {
+        synchronized (mLock) {
+            if (mIsClosed) return;
+            mIsClosed = true;
+
+            mListener.asBinder().unlinkToDeath(mDeathRecipient, 0);
+
+            for (ModuleWatcher watcher : mModuleWatchers) {
+                watcher.close();
+            }
+            mModuleWatchers.clear();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
index fc9a5d6..406231a 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
@@ -18,6 +18,8 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+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;
@@ -81,6 +83,10 @@
         return mModules.containsKey(id);
     }
 
+    public boolean hasAnyModules() {
+        return !mModules.isEmpty();
+    }
+
     public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
         boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException {
         Objects.requireNonNull(callback);
@@ -98,4 +104,22 @@
         session.setConfiguration(legacyConfig);
         return session;
     }
+
+    public ICloseHandle addAnnouncementListener(@NonNull int[] enabledTypes,
+            @NonNull IAnnouncementListener listener) {
+        AnnouncementAggregator aggregator = new AnnouncementAggregator(listener);
+        boolean anySupported = false;
+        for (RadioModule module : mModules.values()) {
+            try {
+                aggregator.watchModule(module, enabledTypes);
+                anySupported = true;
+            } catch (UnsupportedOperationException ex) {
+                Slog.v(TAG, "Announcements not supported for this module", ex);
+            }
+        }
+        if (!anySupported) {
+            Slog.i(TAG, "There are no HAL modules that support announcements");
+        }
+        return aggregator;
+    }
 }
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
index 60a927c..f1e36bb 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.hardware.broadcastradio.V2_0.AmFmBandRange;
 import android.hardware.broadcastradio.V2_0.AmFmRegionConfig;
+import android.hardware.broadcastradio.V2_0.Announcement;
 import android.hardware.broadcastradio.V2_0.ProgramFilter;
 import android.hardware.broadcastradio.V2_0.ProgramIdentifier;
 import android.hardware.broadcastradio.V2_0.ProgramInfo;
@@ -266,4 +267,13 @@
 
         return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed);
     }
+
+    public static @NonNull android.hardware.radio.Announcement announcementFromHal(
+            @NonNull Announcement hwAnnouncement) {
+        return new android.hardware.radio.Announcement(
+            programSelectorFromHal(hwAnnouncement.selector),
+            hwAnnouncement.type,
+            vendorInfoFromHal(hwAnnouncement.vendorInfo)
+        );
+    }
 }
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
index c8e15c1..4dff9e0 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -21,7 +21,10 @@
 import android.hardware.radio.ITuner;
 import android.hardware.radio.RadioManager;
 import android.hardware.broadcastradio.V2_0.AmFmRegionConfig;
+import android.hardware.broadcastradio.V2_0.Announcement;
+import android.hardware.broadcastradio.V2_0.IAnnouncementListener;
 import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.hardware.broadcastradio.V2_0.ICloseHandle;
 import android.hardware.broadcastradio.V2_0.ITunerSession;
 import android.hardware.broadcastradio.V2_0.Result;
 import android.os.ParcelableException;
@@ -29,7 +32,11 @@
 import android.util.MutableInt;
 import android.util.Slog;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 class RadioModule {
     private static final String TAG = "BcRadio2Srv.module";
@@ -79,4 +86,37 @@
 
         return new TunerSession(hwSession.value, cb);
     }
+
+    public android.hardware.radio.ICloseHandle addAnnouncementListener(@NonNull int[] enabledTypes,
+            @NonNull android.hardware.radio.IAnnouncementListener listener) throws RemoteException {
+        ArrayList<Byte> enabledList = new ArrayList<>();
+        for (int type : enabledTypes) {
+            enabledList.add((byte)type);
+        }
+
+        MutableInt halResult = new MutableInt(Result.UNKNOWN_ERROR);
+        Mutable<ICloseHandle> hwCloseHandle = new Mutable<>();
+        IAnnouncementListener hwListener = new IAnnouncementListener.Stub() {
+            public void onListUpdated(ArrayList<Announcement> hwAnnouncements)
+                    throws RemoteException {
+                listener.onListUpdated(hwAnnouncements.stream().
+                    map(a -> Convert.announcementFromHal(a)).collect(Collectors.toList()));
+            }
+        };
+        mService.registerAnnouncementListener(enabledList, hwListener, (result, closeHandle) -> {
+            halResult.value = result;
+            hwCloseHandle.value = closeHandle;
+        });
+        Convert.throwOnError("addAnnouncementListener", halResult.value);
+
+        return new android.hardware.radio.ICloseHandle.Stub() {
+            public void close() {
+                try {
+                    hwCloseHandle.value.close();
+                } catch (RemoteException ex) {
+                    Slog.e(TAG, "Failed closing announcement listener", ex);
+                }
+            }
+        };
+    }
 }