Implement front-end APIs for announcements.

Bug: 68045105
Test: instrumentation (none added)
Change-Id: I602e8bb0c40516a732d606f745c8f7721583155f
This commit is contained in:
Tomasz Wasilczyk 2018-01-11 16:03:46 -08:00
parent 129f8ae2e8
commit f151a7b337
14 changed files with 534 additions and 1 deletions

View File

@ -177,6 +177,8 @@ java_library {
"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",

View File

@ -1739,6 +1739,27 @@ package android.hardware.location {
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 @@ package android.hardware.radio {
}
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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 @@ interface IRadioService {
ITuner openTuner(int moduleId, in RadioManager.BandConfig bandConfig, boolean withAudio,
in ITunerCallback callback);
ICloseHandle addAnnouncementListener(in int[] enabledTypes,
in IAnnouncementListener listener);
}

View File

@ -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.List;
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 @@ public class RadioManager {
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;

View File

@ -20,6 +20,7 @@ import android.annotation.NonNull;
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.Objects;
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 @@ final class Utils {
}
});
}
static void close(ICloseHandle handle) {
try {
handle.close();
} catch (RemoteException ex) {
ex.rethrowFromSystemServer();
}
}
}

View File

@ -20,6 +20,8 @@ import android.annotation.NonNull;
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.ParcelableException;
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 @@ public class BroadcastRadioService extends SystemService {
@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 @@ public class BroadcastRadioService extends SystemService {
}
}
}
@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);
}
}
}
}

View File

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

View File

@ -18,6 +18,8 @@ package com.android.server.broadcastradio.hal2;
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 @@ public class BroadcastRadioService {
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 @@ public class BroadcastRadioService {
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;
}
}

View File

@ -20,6 +20,7 @@ import android.annotation.NonNull;
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 @@ class Convert {
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)
);
}
}

View File

@ -21,7 +21,10 @@ import android.annotation.Nullable;
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.os.RemoteException;
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 @@ class RadioModule {
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);
}
}
};
}
}