Loading api/system-current.txt +5 −1 Original line number Original line Diff line number Diff line Loading @@ -1804,6 +1804,7 @@ package android.hardware.radio { method public deprecated int getProgramType(); method public deprecated int getProgramType(); method public android.hardware.radio.ProgramSelector.Identifier[] getSecondaryIds(); method public android.hardware.radio.ProgramSelector.Identifier[] getSecondaryIds(); method public deprecated long[] getVendorIds(); method public deprecated long[] getVendorIds(); method public android.hardware.radio.ProgramSelector withSecondaryPreferred(android.hardware.radio.ProgramSelector.Identifier); method public void writeToParcel(android.os.Parcel, int); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector> CREATOR; field public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector> CREATOR; field public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; // 0x1 field public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; // 0x1 Loading Loading @@ -1988,12 +1989,15 @@ package android.hardware.radio { public static class RadioManager.ProgramInfo implements android.os.Parcelable { public static class RadioManager.ProgramInfo implements android.os.Parcelable { method public int describeContents(); method public int describeContents(); method public deprecated int getChannel(); method public deprecated int getChannel(); method public android.hardware.radio.ProgramSelector.Identifier getLogicallyTunedTo(); method public android.hardware.radio.RadioMetadata getMetadata(); method public android.hardware.radio.RadioMetadata getMetadata(); method public android.hardware.radio.ProgramSelector.Identifier getPhysicallyTunedTo(); method public java.util.Collection<android.hardware.radio.ProgramSelector.Identifier> getRelatedContent(); method public android.hardware.radio.ProgramSelector getSelector(); method public android.hardware.radio.ProgramSelector getSelector(); method public int getSignalStrength(); method public int getSignalStrength(); method public deprecated int getSubChannel(); method public deprecated int getSubChannel(); method public java.util.Map<java.lang.String, java.lang.String> getVendorInfo(); method public java.util.Map<java.lang.String, java.lang.String> getVendorInfo(); method public boolean isDigital(); method public deprecated boolean isDigital(); method public boolean isLive(); method public boolean isLive(); method public boolean isMuted(); method public boolean isMuted(); method public boolean isStereo(); method public boolean isStereo(); Loading core/java/android/hardware/radio/ProgramSelector.java +33 −0 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.os.Parcelable; import java.lang.annotation.Retention; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.List; import java.util.Objects; import java.util.Objects; import java.util.stream.Stream; import java.util.stream.Stream; Loading Loading @@ -362,6 +363,38 @@ public final class ProgramSelector implements Parcelable { return mVendorIds; return mVendorIds; } } /** * Creates an equivalent ProgramSelector with a given secondary identifier preferred. * * Used to point to a specific physical identifier for technologies that may broadcast the same * program on different channels. For example, with a DAB program broadcasted over multiple * ensembles, the radio hardware may select the one with the strongest signal. The UI may select * preferred ensemble though, so the radio hardware may try to use it in the first place. * * This is a best-effort hint for the tuner, not a guaranteed behavior. * * Setting the given secondary identifier as preferred means filtering out other secondary * identifiers of its type and adding it to the list. * * @param preferred preferred secondary identifier * @return a new ProgramSelector with a given secondary identifier preferred */ public @NonNull ProgramSelector withSecondaryPreferred(@NonNull Identifier preferred) { int preferredType = preferred.getType(); Identifier[] secondaryIds = Stream.concat( // remove other identifiers of that type Arrays.stream(mSecondaryIds).filter(id -> id.getType() != preferredType), // add preferred identifier instead Stream.of(preferred)).toArray(Identifier[]::new); return new ProgramSelector( mProgramType, mPrimaryId, secondaryIds, mVendorIds ); } /** /** * Builds new ProgramSelector for AM/FM frequency. * Builds new ProgramSelector for AM/FM frequency. * * Loading core/java/android/hardware/radio/RadioManager.java +135 −87 Original line number Original line Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.SystemService; Loading @@ -33,9 +34,13 @@ import android.os.ServiceManager.ServiceNotFoundException; import android.text.TextUtils; import android.text.TextUtils; import android.util.Log; import android.util.Log; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashMap; import java.util.List; import java.util.List; import java.util.Map; import java.util.Map; Loading Loading @@ -1382,35 +1387,44 @@ public class RadioManager { }; }; } } /** Radio program information returned by /** Radio program information. */ * {@link RadioTuner#getProgramInformation(RadioManager.ProgramInfo[])} */ public static class ProgramInfo implements Parcelable { public static class ProgramInfo implements Parcelable { // sourced from hardware/interfaces/broadcastradio/1.1/types.hal // sourced from hardware/interfaces/broadcastradio/2.0/types.hal private static final int FLAG_LIVE = 1 << 0; private static final int FLAG_LIVE = 1 << 0; private static final int FLAG_MUTED = 1 << 1; private static final int FLAG_MUTED = 1 << 1; private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2; private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2; private static final int FLAG_TRAFFIC_ANNOUNCEMENT = 1 << 3; private static final int FLAG_TRAFFIC_ANNOUNCEMENT = 1 << 3; private static final int FLAG_TUNED = 1 << 4; private static final int FLAG_STEREO = 1 << 5; @NonNull private final ProgramSelector mSelector; @NonNull private final ProgramSelector mSelector; private final boolean mTuned; // TODO(b/69958777): replace with mFlags @Nullable private final ProgramSelector.Identifier mLogicallyTunedTo; private final boolean mStereo; @Nullable private final ProgramSelector.Identifier mPhysicallyTunedTo; private final boolean mDigital; @NonNull private final Collection<ProgramSelector.Identifier> mRelatedContent; private final int mFlags; private final int mInfoFlags; private final int mSignalStrength; private final int mSignalQuality; private final RadioMetadata mMetadata; @Nullable private final RadioMetadata mMetadata; @NonNull private final Map<String, String> mVendorInfo; @NonNull private final Map<String, String> mVendorInfo; /** @hide */ /** @hide */ public ProgramInfo(@NonNull ProgramSelector selector, boolean tuned, boolean stereo, public ProgramInfo(@NonNull ProgramSelector selector, boolean digital, int signalStrength, RadioMetadata metadata, int flags, @Nullable ProgramSelector.Identifier logicallyTunedTo, Map<String, String> vendorInfo) { @Nullable ProgramSelector.Identifier physicallyTunedTo, mSelector = selector; @Nullable Collection<ProgramSelector.Identifier> relatedContent, mTuned = tuned; int infoFlags, int signalQuality, @Nullable RadioMetadata metadata, mStereo = stereo; @Nullable Map<String, String> vendorInfo) { mDigital = digital; mSelector = Objects.requireNonNull(selector); mFlags = flags; mLogicallyTunedTo = logicallyTunedTo; mSignalStrength = signalStrength; mPhysicallyTunedTo = physicallyTunedTo; if (relatedContent == null) { mRelatedContent = Collections.emptyList(); } else { Preconditions.checkCollectionElementsNotNull(relatedContent, "relatedContent"); mRelatedContent = relatedContent; } mInfoFlags = infoFlags; mSignalQuality = signalQuality; mMetadata = metadata; mMetadata = metadata; mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo; mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo; } } Loading @@ -1424,6 +1438,51 @@ public class RadioManager { return mSelector; return mSelector; } } /** * Identifier currently used for program selection. * * This identifier can be used to determine which technology is * currently being used for reception. * * Some program selectors contain tuning information for different radio * technologies (i.e. FM RDS and DAB). For example, user may tune using * a ProgramSelector with RDS_PI primary identifier, but the tuner hardware * may choose to use DAB technology to make actual tuning. This identifier * must reflect that. */ public @Nullable ProgramSelector.Identifier getLogicallyTunedTo() { return mLogicallyTunedTo; } /** * Identifier currently used by hardware to physically tune to a channel. * * Some radio technologies broadcast the same program on multiple channels, * i.e. with RDS AF the same program may be broadcasted on multiple * alternative frequencies; the same DAB program may be broadcast on * multiple ensembles. This identifier points to the channel to which the * radio hardware is physically tuned to. */ public @Nullable ProgramSelector.Identifier getPhysicallyTunedTo() { return mPhysicallyTunedTo; } /** * Primary identifiers of related contents. * * Some radio technologies provide pointers to other programs that carry * related content (i.e. DAB soft-links). This field is a list of pointers * to other programs on the program list. * * Please note, that these identifiers does not have to exist on the program * list - i.e. DAB tuner may provide information on FM RDS alternatives * despite not supporting FM RDS. If the system has multiple tuners, another * one may have it on its list. */ public @Nullable Collection<ProgramSelector.Identifier> getRelatedContent() { return mRelatedContent; } /** Main channel expressed in units according to band type. /** Main channel expressed in units according to band type. * Currently all defined band types express channels as frequency in kHz * Currently all defined band types express channels as frequency in kHz * @return the program channel * @return the program channel Loading Loading @@ -1458,19 +1517,28 @@ public class RadioManager { * @return {@code true} if currently tuned, {@code false} otherwise. * @return {@code true} if currently tuned, {@code false} otherwise. */ */ public boolean isTuned() { public boolean isTuned() { return mTuned; return (mInfoFlags & FLAG_TUNED) != 0; } } /** {@code true} if the received program is stereo /** {@code true} if the received program is stereo * @return {@code true} if stereo, {@code false} otherwise. * @return {@code true} if stereo, {@code false} otherwise. */ */ public boolean isStereo() { public boolean isStereo() { return mStereo; return (mInfoFlags & FLAG_STEREO) != 0; } } /** {@code true} if the received program is digital (e.g HD radio) /** {@code true} if the received program is digital (e.g HD radio) * @return {@code true} if digital, {@code false} otherwise. * @return {@code true} if digital, {@code false} otherwise. * @deprecated Use {@link getLogicallyTunedTo()} instead. */ */ @Deprecated public boolean isDigital() { public boolean isDigital() { return mDigital; ProgramSelector.Identifier id = mLogicallyTunedTo; if (id == null) id = mSelector.getPrimaryId(); int type = id.getType(); return (type != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY && type != ProgramSelector.IDENTIFIER_TYPE_RDS_PI); } } /** /** Loading @@ -1479,7 +1547,7 @@ public class RadioManager { * usually targetted at reduced latency. * usually targetted at reduced latency. */ */ public boolean isLive() { public boolean isLive() { return (mFlags & FLAG_LIVE) != 0; return (mInfoFlags & FLAG_LIVE) != 0; } } /** /** Loading @@ -1489,7 +1557,7 @@ public class RadioManager { * It does NOT mean the user has muted audio. * It does NOT mean the user has muted audio. */ */ public boolean isMuted() { public boolean isMuted() { return (mFlags & FLAG_MUTED) != 0; return (mInfoFlags & FLAG_MUTED) != 0; } } /** /** Loading @@ -1497,7 +1565,7 @@ public class RadioManager { * regularily. * regularily. */ */ public boolean isTrafficProgram() { public boolean isTrafficProgram() { return (mFlags & FLAG_TRAFFIC_PROGRAM) != 0; return (mInfoFlags & FLAG_TRAFFIC_PROGRAM) != 0; } } /** /** Loading @@ -1505,15 +1573,18 @@ public class RadioManager { * at the very moment. * at the very moment. */ */ public boolean isTrafficAnnouncementActive() { public boolean isTrafficAnnouncementActive() { return (mFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0; return (mInfoFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0; } } /** Signal strength indicator from 0 (no signal) to 100 (excellent) /** * @return the signal strength indication. * Signal quality (as opposed to the name) indication from 0 (no signal) * to 100 (excellent) * @return the signal quality indication. */ */ public int getSignalStrength() { public int getSignalStrength() { return mSignalStrength; return mSignalQuality; } } /** Metadata currently received from this station. /** Metadata currently received from this station. * null if no metadata have been received * null if no metadata have been received * @return current meta data received from this program. * @return current meta data received from this program. Loading @@ -1537,17 +1608,13 @@ public class RadioManager { } } private ProgramInfo(Parcel in) { private ProgramInfo(Parcel in) { mSelector = in.readParcelable(null); mSelector = Objects.requireNonNull(in.readTypedObject(ProgramSelector.CREATOR)); mTuned = in.readByte() == 1; mLogicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR); mStereo = in.readByte() == 1; mPhysicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR); mDigital = in.readByte() == 1; mRelatedContent = in.createTypedArrayList(ProgramSelector.Identifier.CREATOR); mSignalStrength = in.readInt(); mInfoFlags = in.readInt(); if (in.readByte() == 1) { mSignalQuality = in.readInt(); mMetadata = RadioMetadata.CREATOR.createFromParcel(in); mMetadata = in.readTypedObject(RadioMetadata.CREATOR); } else { mMetadata = null; } mFlags = in.readInt(); mVendorInfo = Utils.readStringMap(in); mVendorInfo = Utils.readStringMap(in); } } Loading @@ -1564,18 +1631,13 @@ public class RadioManager { @Override @Override public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(mSelector, 0); dest.writeTypedObject(mSelector, flags); dest.writeByte((byte)(mTuned ? 1 : 0)); dest.writeTypedObject(mLogicallyTunedTo, flags); dest.writeByte((byte)(mStereo ? 1 : 0)); dest.writeTypedObject(mPhysicallyTunedTo, flags); dest.writeByte((byte)(mDigital ? 1 : 0)); Utils.writeTypedCollection(dest, mRelatedContent); dest.writeInt(mSignalStrength); dest.writeInt(mInfoFlags); if (mMetadata == null) { dest.writeInt(mSignalQuality); dest.writeByte((byte)0); dest.writeTypedObject(mMetadata, flags); } else { dest.writeByte((byte)1); mMetadata.writeToParcel(dest, flags); } dest.writeInt(mFlags); Utils.writeStringMap(dest, mVendorInfo); Utils.writeStringMap(dest, mVendorInfo); } } Loading @@ -1586,52 +1648,38 @@ public class RadioManager { @Override @Override public String toString() { public String toString() { return "ProgramInfo [mSelector=" + mSelector return "ProgramInfo" + ", mTuned=" + mTuned + ", mStereo=" + mStereo + ", mDigital=" + mDigital + " [selector=" + mSelector + ", mFlags=" + mFlags + ", mSignalStrength=" + mSignalStrength + ", logicallyTunedTo=" + Objects.toString(mLogicallyTunedTo) + ((mMetadata == null) ? "" : (", mMetadata=" + mMetadata.toString())) + ", physicallyTunedTo=" + Objects.toString(mPhysicallyTunedTo) + ", relatedContent=" + mRelatedContent.size() + ", infoFlags=" + mInfoFlags + ", mSignalQuality=" + mSignalQuality + ", mMetadata=" + Objects.toString(mMetadata) + "]"; + "]"; } } @Override @Override public int hashCode() { public int hashCode() { final int prime = 31; return Objects.hash(mSelector, mLogicallyTunedTo, mPhysicallyTunedTo, int result = 1; mRelatedContent, mInfoFlags, mSignalQuality, mMetadata, mVendorInfo); result = prime * result + mSelector.hashCode(); result = prime * result + (mTuned ? 1 : 0); result = prime * result + (mStereo ? 1 : 0); result = prime * result + (mDigital ? 1 : 0); result = prime * result + mFlags; result = prime * result + mSignalStrength; result = prime * result + ((mMetadata == null) ? 0 : mMetadata.hashCode()); result = prime * result + mVendorInfo.hashCode(); return result; } } @Override @Override public boolean equals(Object obj) { public boolean equals(Object obj) { if (this == obj) if (this == obj) return true; return true; if (!(obj instanceof ProgramInfo)) return false; if (!(obj instanceof ProgramInfo)) return false; ProgramInfo other = (ProgramInfo) obj; ProgramInfo other = (ProgramInfo) obj; if (!mSelector.equals(other.getSelector())) return false; if (mTuned != other.isTuned()) if (!Objects.equals(mSelector, other.mSelector)) return false; return false; if (!Objects.equals(mLogicallyTunedTo, other.mLogicallyTunedTo)) return false; if (mStereo != other.isStereo()) if (!Objects.equals(mPhysicallyTunedTo, other.mPhysicallyTunedTo)) return false; return false; if (!Objects.equals(mRelatedContent, other.mRelatedContent)) return false; if (mDigital != other.isDigital()) if (mInfoFlags != other.mInfoFlags) return false; return false; if (mSignalQuality != other.mSignalQuality) return false; if (mFlags != other.mFlags) if (!Objects.equals(mMetadata, other.mMetadata)) return false; return false; if (!Objects.equals(mVendorInfo, other.mVendorInfo)) return false; if (mSignalStrength != other.getSignalStrength()) return false; if (mMetadata == null) { if (other.getMetadata() != null) return false; } else if (!mMetadata.equals(other.getMetadata())) return false; if (!mVendorInfo.equals(other.mVendorInfo)) return false; return true; return true; } } } } Loading core/java/android/hardware/radio/Utils.java +15 −0 Original line number Original line Diff line number Diff line Loading @@ -22,6 +22,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.Parcelable; import android.os.RemoteException; import android.os.RemoteException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashMap; import java.util.HashSet; import java.util.HashSet; import java.util.Map; import java.util.Map; Loading Loading @@ -93,6 +95,19 @@ final class Utils { }); }); } } static <T extends Parcelable> void writeTypedCollection(@NonNull Parcel dest, @Nullable Collection<T> coll) { ArrayList<T> list = null; if (coll != null) { if (coll instanceof ArrayList) { list = (ArrayList) coll; } else { list = new ArrayList<>(coll); } } dest.writeTypedList(list); } static void close(ICloseHandle handle) { static void close(ICloseHandle handle) { try { try { handle.close(); handle.close(); Loading services/core/java/com/android/server/broadcastradio/hal2/Convert.java +28 −18 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.hardware.broadcastradio.V2_0.AmFmBandRange; import android.hardware.broadcastradio.V2_0.AmFmBandRange; import android.hardware.broadcastradio.V2_0.AmFmRegionConfig; import android.hardware.broadcastradio.V2_0.AmFmRegionConfig; import android.hardware.broadcastradio.V2_0.Announcement; import android.hardware.broadcastradio.V2_0.Announcement; import android.hardware.broadcastradio.V2_0.IdentifierType; import android.hardware.broadcastradio.V2_0.ProgramFilter; import android.hardware.broadcastradio.V2_0.ProgramFilter; import android.hardware.broadcastradio.V2_0.ProgramIdentifier; import android.hardware.broadcastradio.V2_0.ProgramIdentifier; import android.hardware.broadcastradio.V2_0.ProgramInfo; import android.hardware.broadcastradio.V2_0.ProgramInfo; Loading @@ -37,6 +38,7 @@ import android.util.Slog; import java.util.ArrayList; import java.util.ArrayList; import java.util.Arrays; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Collections; import java.util.HashMap; import java.util.HashMap; import java.util.HashSet; import java.util.HashSet; Loading Loading @@ -219,30 +221,37 @@ class Convert { return hwId; return hwId; } } static @NonNull ProgramSelector.Identifier programIdentifierFromHal(@NonNull ProgramIdentifier id) { static @Nullable ProgramSelector.Identifier programIdentifierFromHal( @NonNull ProgramIdentifier id) { if (id.type == IdentifierType.INVALID) return null; return new ProgramSelector.Identifier(id.type, id.value); return new ProgramSelector.Identifier(id.type, id.value); } } static @NonNull ProgramSelector programSelectorFromHal( static @NonNull ProgramSelector programSelectorFromHal( @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) { @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) { ProgramSelector.Identifier[] secondaryIds = sel.secondaryIds.stream().map( ProgramSelector.Identifier[] secondaryIds = sel.secondaryIds.stream(). id -> programIdentifierFromHal(id)).toArray(ProgramSelector.Identifier[]::new); map(id -> Objects.requireNonNull(programIdentifierFromHal(id))). toArray(ProgramSelector.Identifier[]::new); return new ProgramSelector( return new ProgramSelector( identifierTypeToProgramType(sel.primaryId.type), identifierTypeToProgramType(sel.primaryId.type), programIdentifierFromHal(sel.primaryId), Objects.requireNonNull(programIdentifierFromHal(sel.primaryId)), secondaryIds, null); secondaryIds, null); } } static @NonNull RadioManager.ProgramInfo programInfoFromHal(@NonNull ProgramInfo info) { static @NonNull RadioManager.ProgramInfo programInfoFromHal(@NonNull ProgramInfo info) { Collection<ProgramSelector.Identifier> relatedContent = info.relatedContent.stream(). map(id -> Objects.requireNonNull(programIdentifierFromHal(id))). collect(Collectors.toList()); return new RadioManager.ProgramInfo( return new RadioManager.ProgramInfo( programSelectorFromHal(info.selector), programSelectorFromHal(info.selector), (info.infoFlags & ProgramInfoFlags.TUNED) != 0, programIdentifierFromHal(info.logicallyTunedTo), (info.infoFlags & ProgramInfoFlags.STEREO) != 0, programIdentifierFromHal(info.physicallyTunedTo), false, // TODO(b/69860743): digital relatedContent, info.infoFlags, info.signalQuality, info.signalQuality, null, // TODO(b/69860743): metadata null, // TODO(b/69860743): metadata info.infoFlags, vendorInfoFromHal(info.vendorInfo) vendorInfoFromHal(info.vendorInfo) ); ); } } Loading @@ -260,10 +269,11 @@ class Convert { } } static @NonNull ProgramList.Chunk programListChunkFromHal(@NonNull ProgramListChunk chunk) { static @NonNull ProgramList.Chunk programListChunkFromHal(@NonNull ProgramListChunk chunk) { Set<RadioManager.ProgramInfo> modified = chunk.modified.stream().map( Set<RadioManager.ProgramInfo> modified = chunk.modified.stream(). info -> programInfoFromHal(info)).collect(Collectors.toSet()); map(info -> programInfoFromHal(info)).collect(Collectors.toSet()); Set<ProgramSelector.Identifier> removed = chunk.removed.stream().map( Set<ProgramSelector.Identifier> removed = chunk.removed.stream(). id -> programIdentifierFromHal(id)).collect(Collectors.toSet()); map(id -> Objects.requireNonNull(programIdentifierFromHal(id))). collect(Collectors.toSet()); return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed); return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed); } } Loading Loading
api/system-current.txt +5 −1 Original line number Original line Diff line number Diff line Loading @@ -1804,6 +1804,7 @@ package android.hardware.radio { method public deprecated int getProgramType(); method public deprecated int getProgramType(); method public android.hardware.radio.ProgramSelector.Identifier[] getSecondaryIds(); method public android.hardware.radio.ProgramSelector.Identifier[] getSecondaryIds(); method public deprecated long[] getVendorIds(); method public deprecated long[] getVendorIds(); method public android.hardware.radio.ProgramSelector withSecondaryPreferred(android.hardware.radio.ProgramSelector.Identifier); method public void writeToParcel(android.os.Parcel, int); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector> CREATOR; field public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector> CREATOR; field public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; // 0x1 field public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; // 0x1 Loading Loading @@ -1988,12 +1989,15 @@ package android.hardware.radio { public static class RadioManager.ProgramInfo implements android.os.Parcelable { public static class RadioManager.ProgramInfo implements android.os.Parcelable { method public int describeContents(); method public int describeContents(); method public deprecated int getChannel(); method public deprecated int getChannel(); method public android.hardware.radio.ProgramSelector.Identifier getLogicallyTunedTo(); method public android.hardware.radio.RadioMetadata getMetadata(); method public android.hardware.radio.RadioMetadata getMetadata(); method public android.hardware.radio.ProgramSelector.Identifier getPhysicallyTunedTo(); method public java.util.Collection<android.hardware.radio.ProgramSelector.Identifier> getRelatedContent(); method public android.hardware.radio.ProgramSelector getSelector(); method public android.hardware.radio.ProgramSelector getSelector(); method public int getSignalStrength(); method public int getSignalStrength(); method public deprecated int getSubChannel(); method public deprecated int getSubChannel(); method public java.util.Map<java.lang.String, java.lang.String> getVendorInfo(); method public java.util.Map<java.lang.String, java.lang.String> getVendorInfo(); method public boolean isDigital(); method public deprecated boolean isDigital(); method public boolean isLive(); method public boolean isLive(); method public boolean isMuted(); method public boolean isMuted(); method public boolean isStereo(); method public boolean isStereo(); Loading
core/java/android/hardware/radio/ProgramSelector.java +33 −0 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.os.Parcelable; import java.lang.annotation.Retention; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.List; import java.util.Objects; import java.util.Objects; import java.util.stream.Stream; import java.util.stream.Stream; Loading Loading @@ -362,6 +363,38 @@ public final class ProgramSelector implements Parcelable { return mVendorIds; return mVendorIds; } } /** * Creates an equivalent ProgramSelector with a given secondary identifier preferred. * * Used to point to a specific physical identifier for technologies that may broadcast the same * program on different channels. For example, with a DAB program broadcasted over multiple * ensembles, the radio hardware may select the one with the strongest signal. The UI may select * preferred ensemble though, so the radio hardware may try to use it in the first place. * * This is a best-effort hint for the tuner, not a guaranteed behavior. * * Setting the given secondary identifier as preferred means filtering out other secondary * identifiers of its type and adding it to the list. * * @param preferred preferred secondary identifier * @return a new ProgramSelector with a given secondary identifier preferred */ public @NonNull ProgramSelector withSecondaryPreferred(@NonNull Identifier preferred) { int preferredType = preferred.getType(); Identifier[] secondaryIds = Stream.concat( // remove other identifiers of that type Arrays.stream(mSecondaryIds).filter(id -> id.getType() != preferredType), // add preferred identifier instead Stream.of(preferred)).toArray(Identifier[]::new); return new ProgramSelector( mProgramType, mPrimaryId, secondaryIds, mVendorIds ); } /** /** * Builds new ProgramSelector for AM/FM frequency. * Builds new ProgramSelector for AM/FM frequency. * * Loading
core/java/android/hardware/radio/RadioManager.java +135 −87 Original line number Original line Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.SystemService; Loading @@ -33,9 +34,13 @@ import android.os.ServiceManager.ServiceNotFoundException; import android.text.TextUtils; import android.text.TextUtils; import android.util.Log; import android.util.Log; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashMap; import java.util.List; import java.util.List; import java.util.Map; import java.util.Map; Loading Loading @@ -1382,35 +1387,44 @@ public class RadioManager { }; }; } } /** Radio program information returned by /** Radio program information. */ * {@link RadioTuner#getProgramInformation(RadioManager.ProgramInfo[])} */ public static class ProgramInfo implements Parcelable { public static class ProgramInfo implements Parcelable { // sourced from hardware/interfaces/broadcastradio/1.1/types.hal // sourced from hardware/interfaces/broadcastradio/2.0/types.hal private static final int FLAG_LIVE = 1 << 0; private static final int FLAG_LIVE = 1 << 0; private static final int FLAG_MUTED = 1 << 1; private static final int FLAG_MUTED = 1 << 1; private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2; private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2; private static final int FLAG_TRAFFIC_ANNOUNCEMENT = 1 << 3; private static final int FLAG_TRAFFIC_ANNOUNCEMENT = 1 << 3; private static final int FLAG_TUNED = 1 << 4; private static final int FLAG_STEREO = 1 << 5; @NonNull private final ProgramSelector mSelector; @NonNull private final ProgramSelector mSelector; private final boolean mTuned; // TODO(b/69958777): replace with mFlags @Nullable private final ProgramSelector.Identifier mLogicallyTunedTo; private final boolean mStereo; @Nullable private final ProgramSelector.Identifier mPhysicallyTunedTo; private final boolean mDigital; @NonNull private final Collection<ProgramSelector.Identifier> mRelatedContent; private final int mFlags; private final int mInfoFlags; private final int mSignalStrength; private final int mSignalQuality; private final RadioMetadata mMetadata; @Nullable private final RadioMetadata mMetadata; @NonNull private final Map<String, String> mVendorInfo; @NonNull private final Map<String, String> mVendorInfo; /** @hide */ /** @hide */ public ProgramInfo(@NonNull ProgramSelector selector, boolean tuned, boolean stereo, public ProgramInfo(@NonNull ProgramSelector selector, boolean digital, int signalStrength, RadioMetadata metadata, int flags, @Nullable ProgramSelector.Identifier logicallyTunedTo, Map<String, String> vendorInfo) { @Nullable ProgramSelector.Identifier physicallyTunedTo, mSelector = selector; @Nullable Collection<ProgramSelector.Identifier> relatedContent, mTuned = tuned; int infoFlags, int signalQuality, @Nullable RadioMetadata metadata, mStereo = stereo; @Nullable Map<String, String> vendorInfo) { mDigital = digital; mSelector = Objects.requireNonNull(selector); mFlags = flags; mLogicallyTunedTo = logicallyTunedTo; mSignalStrength = signalStrength; mPhysicallyTunedTo = physicallyTunedTo; if (relatedContent == null) { mRelatedContent = Collections.emptyList(); } else { Preconditions.checkCollectionElementsNotNull(relatedContent, "relatedContent"); mRelatedContent = relatedContent; } mInfoFlags = infoFlags; mSignalQuality = signalQuality; mMetadata = metadata; mMetadata = metadata; mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo; mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo; } } Loading @@ -1424,6 +1438,51 @@ public class RadioManager { return mSelector; return mSelector; } } /** * Identifier currently used for program selection. * * This identifier can be used to determine which technology is * currently being used for reception. * * Some program selectors contain tuning information for different radio * technologies (i.e. FM RDS and DAB). For example, user may tune using * a ProgramSelector with RDS_PI primary identifier, but the tuner hardware * may choose to use DAB technology to make actual tuning. This identifier * must reflect that. */ public @Nullable ProgramSelector.Identifier getLogicallyTunedTo() { return mLogicallyTunedTo; } /** * Identifier currently used by hardware to physically tune to a channel. * * Some radio technologies broadcast the same program on multiple channels, * i.e. with RDS AF the same program may be broadcasted on multiple * alternative frequencies; the same DAB program may be broadcast on * multiple ensembles. This identifier points to the channel to which the * radio hardware is physically tuned to. */ public @Nullable ProgramSelector.Identifier getPhysicallyTunedTo() { return mPhysicallyTunedTo; } /** * Primary identifiers of related contents. * * Some radio technologies provide pointers to other programs that carry * related content (i.e. DAB soft-links). This field is a list of pointers * to other programs on the program list. * * Please note, that these identifiers does not have to exist on the program * list - i.e. DAB tuner may provide information on FM RDS alternatives * despite not supporting FM RDS. If the system has multiple tuners, another * one may have it on its list. */ public @Nullable Collection<ProgramSelector.Identifier> getRelatedContent() { return mRelatedContent; } /** Main channel expressed in units according to band type. /** Main channel expressed in units according to band type. * Currently all defined band types express channels as frequency in kHz * Currently all defined band types express channels as frequency in kHz * @return the program channel * @return the program channel Loading Loading @@ -1458,19 +1517,28 @@ public class RadioManager { * @return {@code true} if currently tuned, {@code false} otherwise. * @return {@code true} if currently tuned, {@code false} otherwise. */ */ public boolean isTuned() { public boolean isTuned() { return mTuned; return (mInfoFlags & FLAG_TUNED) != 0; } } /** {@code true} if the received program is stereo /** {@code true} if the received program is stereo * @return {@code true} if stereo, {@code false} otherwise. * @return {@code true} if stereo, {@code false} otherwise. */ */ public boolean isStereo() { public boolean isStereo() { return mStereo; return (mInfoFlags & FLAG_STEREO) != 0; } } /** {@code true} if the received program is digital (e.g HD radio) /** {@code true} if the received program is digital (e.g HD radio) * @return {@code true} if digital, {@code false} otherwise. * @return {@code true} if digital, {@code false} otherwise. * @deprecated Use {@link getLogicallyTunedTo()} instead. */ */ @Deprecated public boolean isDigital() { public boolean isDigital() { return mDigital; ProgramSelector.Identifier id = mLogicallyTunedTo; if (id == null) id = mSelector.getPrimaryId(); int type = id.getType(); return (type != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY && type != ProgramSelector.IDENTIFIER_TYPE_RDS_PI); } } /** /** Loading @@ -1479,7 +1547,7 @@ public class RadioManager { * usually targetted at reduced latency. * usually targetted at reduced latency. */ */ public boolean isLive() { public boolean isLive() { return (mFlags & FLAG_LIVE) != 0; return (mInfoFlags & FLAG_LIVE) != 0; } } /** /** Loading @@ -1489,7 +1557,7 @@ public class RadioManager { * It does NOT mean the user has muted audio. * It does NOT mean the user has muted audio. */ */ public boolean isMuted() { public boolean isMuted() { return (mFlags & FLAG_MUTED) != 0; return (mInfoFlags & FLAG_MUTED) != 0; } } /** /** Loading @@ -1497,7 +1565,7 @@ public class RadioManager { * regularily. * regularily. */ */ public boolean isTrafficProgram() { public boolean isTrafficProgram() { return (mFlags & FLAG_TRAFFIC_PROGRAM) != 0; return (mInfoFlags & FLAG_TRAFFIC_PROGRAM) != 0; } } /** /** Loading @@ -1505,15 +1573,18 @@ public class RadioManager { * at the very moment. * at the very moment. */ */ public boolean isTrafficAnnouncementActive() { public boolean isTrafficAnnouncementActive() { return (mFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0; return (mInfoFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0; } } /** Signal strength indicator from 0 (no signal) to 100 (excellent) /** * @return the signal strength indication. * Signal quality (as opposed to the name) indication from 0 (no signal) * to 100 (excellent) * @return the signal quality indication. */ */ public int getSignalStrength() { public int getSignalStrength() { return mSignalStrength; return mSignalQuality; } } /** Metadata currently received from this station. /** Metadata currently received from this station. * null if no metadata have been received * null if no metadata have been received * @return current meta data received from this program. * @return current meta data received from this program. Loading @@ -1537,17 +1608,13 @@ public class RadioManager { } } private ProgramInfo(Parcel in) { private ProgramInfo(Parcel in) { mSelector = in.readParcelable(null); mSelector = Objects.requireNonNull(in.readTypedObject(ProgramSelector.CREATOR)); mTuned = in.readByte() == 1; mLogicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR); mStereo = in.readByte() == 1; mPhysicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR); mDigital = in.readByte() == 1; mRelatedContent = in.createTypedArrayList(ProgramSelector.Identifier.CREATOR); mSignalStrength = in.readInt(); mInfoFlags = in.readInt(); if (in.readByte() == 1) { mSignalQuality = in.readInt(); mMetadata = RadioMetadata.CREATOR.createFromParcel(in); mMetadata = in.readTypedObject(RadioMetadata.CREATOR); } else { mMetadata = null; } mFlags = in.readInt(); mVendorInfo = Utils.readStringMap(in); mVendorInfo = Utils.readStringMap(in); } } Loading @@ -1564,18 +1631,13 @@ public class RadioManager { @Override @Override public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(mSelector, 0); dest.writeTypedObject(mSelector, flags); dest.writeByte((byte)(mTuned ? 1 : 0)); dest.writeTypedObject(mLogicallyTunedTo, flags); dest.writeByte((byte)(mStereo ? 1 : 0)); dest.writeTypedObject(mPhysicallyTunedTo, flags); dest.writeByte((byte)(mDigital ? 1 : 0)); Utils.writeTypedCollection(dest, mRelatedContent); dest.writeInt(mSignalStrength); dest.writeInt(mInfoFlags); if (mMetadata == null) { dest.writeInt(mSignalQuality); dest.writeByte((byte)0); dest.writeTypedObject(mMetadata, flags); } else { dest.writeByte((byte)1); mMetadata.writeToParcel(dest, flags); } dest.writeInt(mFlags); Utils.writeStringMap(dest, mVendorInfo); Utils.writeStringMap(dest, mVendorInfo); } } Loading @@ -1586,52 +1648,38 @@ public class RadioManager { @Override @Override public String toString() { public String toString() { return "ProgramInfo [mSelector=" + mSelector return "ProgramInfo" + ", mTuned=" + mTuned + ", mStereo=" + mStereo + ", mDigital=" + mDigital + " [selector=" + mSelector + ", mFlags=" + mFlags + ", mSignalStrength=" + mSignalStrength + ", logicallyTunedTo=" + Objects.toString(mLogicallyTunedTo) + ((mMetadata == null) ? "" : (", mMetadata=" + mMetadata.toString())) + ", physicallyTunedTo=" + Objects.toString(mPhysicallyTunedTo) + ", relatedContent=" + mRelatedContent.size() + ", infoFlags=" + mInfoFlags + ", mSignalQuality=" + mSignalQuality + ", mMetadata=" + Objects.toString(mMetadata) + "]"; + "]"; } } @Override @Override public int hashCode() { public int hashCode() { final int prime = 31; return Objects.hash(mSelector, mLogicallyTunedTo, mPhysicallyTunedTo, int result = 1; mRelatedContent, mInfoFlags, mSignalQuality, mMetadata, mVendorInfo); result = prime * result + mSelector.hashCode(); result = prime * result + (mTuned ? 1 : 0); result = prime * result + (mStereo ? 1 : 0); result = prime * result + (mDigital ? 1 : 0); result = prime * result + mFlags; result = prime * result + mSignalStrength; result = prime * result + ((mMetadata == null) ? 0 : mMetadata.hashCode()); result = prime * result + mVendorInfo.hashCode(); return result; } } @Override @Override public boolean equals(Object obj) { public boolean equals(Object obj) { if (this == obj) if (this == obj) return true; return true; if (!(obj instanceof ProgramInfo)) return false; if (!(obj instanceof ProgramInfo)) return false; ProgramInfo other = (ProgramInfo) obj; ProgramInfo other = (ProgramInfo) obj; if (!mSelector.equals(other.getSelector())) return false; if (mTuned != other.isTuned()) if (!Objects.equals(mSelector, other.mSelector)) return false; return false; if (!Objects.equals(mLogicallyTunedTo, other.mLogicallyTunedTo)) return false; if (mStereo != other.isStereo()) if (!Objects.equals(mPhysicallyTunedTo, other.mPhysicallyTunedTo)) return false; return false; if (!Objects.equals(mRelatedContent, other.mRelatedContent)) return false; if (mDigital != other.isDigital()) if (mInfoFlags != other.mInfoFlags) return false; return false; if (mSignalQuality != other.mSignalQuality) return false; if (mFlags != other.mFlags) if (!Objects.equals(mMetadata, other.mMetadata)) return false; return false; if (!Objects.equals(mVendorInfo, other.mVendorInfo)) return false; if (mSignalStrength != other.getSignalStrength()) return false; if (mMetadata == null) { if (other.getMetadata() != null) return false; } else if (!mMetadata.equals(other.getMetadata())) return false; if (!mVendorInfo.equals(other.mVendorInfo)) return false; return true; return true; } } } } Loading
core/java/android/hardware/radio/Utils.java +15 −0 Original line number Original line Diff line number Diff line Loading @@ -22,6 +22,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.Parcelable; import android.os.RemoteException; import android.os.RemoteException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashMap; import java.util.HashSet; import java.util.HashSet; import java.util.Map; import java.util.Map; Loading Loading @@ -93,6 +95,19 @@ final class Utils { }); }); } } static <T extends Parcelable> void writeTypedCollection(@NonNull Parcel dest, @Nullable Collection<T> coll) { ArrayList<T> list = null; if (coll != null) { if (coll instanceof ArrayList) { list = (ArrayList) coll; } else { list = new ArrayList<>(coll); } } dest.writeTypedList(list); } static void close(ICloseHandle handle) { static void close(ICloseHandle handle) { try { try { handle.close(); handle.close(); Loading
services/core/java/com/android/server/broadcastradio/hal2/Convert.java +28 −18 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.hardware.broadcastradio.V2_0.AmFmBandRange; import android.hardware.broadcastradio.V2_0.AmFmBandRange; import android.hardware.broadcastradio.V2_0.AmFmRegionConfig; import android.hardware.broadcastradio.V2_0.AmFmRegionConfig; import android.hardware.broadcastradio.V2_0.Announcement; import android.hardware.broadcastradio.V2_0.Announcement; import android.hardware.broadcastradio.V2_0.IdentifierType; import android.hardware.broadcastradio.V2_0.ProgramFilter; import android.hardware.broadcastradio.V2_0.ProgramFilter; import android.hardware.broadcastradio.V2_0.ProgramIdentifier; import android.hardware.broadcastradio.V2_0.ProgramIdentifier; import android.hardware.broadcastradio.V2_0.ProgramInfo; import android.hardware.broadcastradio.V2_0.ProgramInfo; Loading @@ -37,6 +38,7 @@ import android.util.Slog; import java.util.ArrayList; import java.util.ArrayList; import java.util.Arrays; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Collections; import java.util.HashMap; import java.util.HashMap; import java.util.HashSet; import java.util.HashSet; Loading Loading @@ -219,30 +221,37 @@ class Convert { return hwId; return hwId; } } static @NonNull ProgramSelector.Identifier programIdentifierFromHal(@NonNull ProgramIdentifier id) { static @Nullable ProgramSelector.Identifier programIdentifierFromHal( @NonNull ProgramIdentifier id) { if (id.type == IdentifierType.INVALID) return null; return new ProgramSelector.Identifier(id.type, id.value); return new ProgramSelector.Identifier(id.type, id.value); } } static @NonNull ProgramSelector programSelectorFromHal( static @NonNull ProgramSelector programSelectorFromHal( @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) { @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) { ProgramSelector.Identifier[] secondaryIds = sel.secondaryIds.stream().map( ProgramSelector.Identifier[] secondaryIds = sel.secondaryIds.stream(). id -> programIdentifierFromHal(id)).toArray(ProgramSelector.Identifier[]::new); map(id -> Objects.requireNonNull(programIdentifierFromHal(id))). toArray(ProgramSelector.Identifier[]::new); return new ProgramSelector( return new ProgramSelector( identifierTypeToProgramType(sel.primaryId.type), identifierTypeToProgramType(sel.primaryId.type), programIdentifierFromHal(sel.primaryId), Objects.requireNonNull(programIdentifierFromHal(sel.primaryId)), secondaryIds, null); secondaryIds, null); } } static @NonNull RadioManager.ProgramInfo programInfoFromHal(@NonNull ProgramInfo info) { static @NonNull RadioManager.ProgramInfo programInfoFromHal(@NonNull ProgramInfo info) { Collection<ProgramSelector.Identifier> relatedContent = info.relatedContent.stream(). map(id -> Objects.requireNonNull(programIdentifierFromHal(id))). collect(Collectors.toList()); return new RadioManager.ProgramInfo( return new RadioManager.ProgramInfo( programSelectorFromHal(info.selector), programSelectorFromHal(info.selector), (info.infoFlags & ProgramInfoFlags.TUNED) != 0, programIdentifierFromHal(info.logicallyTunedTo), (info.infoFlags & ProgramInfoFlags.STEREO) != 0, programIdentifierFromHal(info.physicallyTunedTo), false, // TODO(b/69860743): digital relatedContent, info.infoFlags, info.signalQuality, info.signalQuality, null, // TODO(b/69860743): metadata null, // TODO(b/69860743): metadata info.infoFlags, vendorInfoFromHal(info.vendorInfo) vendorInfoFromHal(info.vendorInfo) ); ); } } Loading @@ -260,10 +269,11 @@ class Convert { } } static @NonNull ProgramList.Chunk programListChunkFromHal(@NonNull ProgramListChunk chunk) { static @NonNull ProgramList.Chunk programListChunkFromHal(@NonNull ProgramListChunk chunk) { Set<RadioManager.ProgramInfo> modified = chunk.modified.stream().map( Set<RadioManager.ProgramInfo> modified = chunk.modified.stream(). info -> programInfoFromHal(info)).collect(Collectors.toSet()); map(info -> programInfoFromHal(info)).collect(Collectors.toSet()); Set<ProgramSelector.Identifier> removed = chunk.removed.stream().map( Set<ProgramSelector.Identifier> removed = chunk.removed.stream(). id -> programIdentifierFromHal(id)).collect(Collectors.toSet()); map(id -> Objects.requireNonNull(programIdentifierFromHal(id))). collect(Collectors.toSet()); return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed); return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed); } } Loading