1 /** 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.hardware.radio; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SystemApi; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 26 import java.util.ArrayList; 27 import java.util.Collections; 28 import java.util.HashMap; 29 import java.util.HashSet; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Objects; 33 import java.util.Set; 34 import java.util.concurrent.Executor; 35 import java.util.stream.Collectors; 36 37 /** 38 * @hide 39 */ 40 @SystemApi 41 public final class ProgramList implements AutoCloseable { 42 43 private final Object mLock = new Object(); 44 private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mPrograms = 45 new HashMap<>(); 46 47 private final List<ListCallback> mListCallbacks = new ArrayList<>(); 48 private final List<OnCompleteListener> mOnCompleteListeners = new ArrayList<>(); 49 private OnCloseListener mOnCloseListener; 50 private boolean mIsClosed = false; 51 private boolean mIsComplete = false; 52 ProgramList()53 ProgramList() {} 54 55 /** 56 * Callback for list change operations. 57 */ 58 public abstract static class ListCallback { 59 /** 60 * Called when item was modified or added to the list. 61 */ onItemChanged(@onNull ProgramSelector.Identifier id)62 public void onItemChanged(@NonNull ProgramSelector.Identifier id) { } 63 64 /** 65 * Called when item was removed from the list. 66 */ onItemRemoved(@onNull ProgramSelector.Identifier id)67 public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { } 68 } 69 70 /** 71 * Listener of list complete event. 72 */ 73 public interface OnCompleteListener { 74 /** 75 * Called when the list turned complete (i.e. when the scan process 76 * came to an end). 77 */ onComplete()78 void onComplete(); 79 } 80 81 interface OnCloseListener { onClose()82 void onClose(); 83 } 84 85 /** 86 * Registers list change callback with executor. 87 */ registerListCallback(@onNull @allbackExecutor Executor executor, @NonNull ListCallback callback)88 public void registerListCallback(@NonNull @CallbackExecutor Executor executor, 89 @NonNull ListCallback callback) { 90 registerListCallback(new ListCallback() { 91 public void onItemChanged(@NonNull ProgramSelector.Identifier id) { 92 executor.execute(() -> callback.onItemChanged(id)); 93 } 94 95 public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { 96 executor.execute(() -> callback.onItemRemoved(id)); 97 } 98 }); 99 } 100 101 /** 102 * Registers list change callback. 103 */ registerListCallback(@onNull ListCallback callback)104 public void registerListCallback(@NonNull ListCallback callback) { 105 synchronized (mLock) { 106 if (mIsClosed) return; 107 mListCallbacks.add(Objects.requireNonNull(callback)); 108 } 109 } 110 111 /** 112 * Unregisters list change callback. 113 */ unregisterListCallback(@onNull ListCallback callback)114 public void unregisterListCallback(@NonNull ListCallback callback) { 115 synchronized (mLock) { 116 if (mIsClosed) return; 117 mListCallbacks.remove(Objects.requireNonNull(callback)); 118 } 119 } 120 121 /** 122 * Adds list complete event listener with executor. 123 */ addOnCompleteListener(@onNull @allbackExecutor Executor executor, @NonNull OnCompleteListener listener)124 public void addOnCompleteListener(@NonNull @CallbackExecutor Executor executor, 125 @NonNull OnCompleteListener listener) { 126 addOnCompleteListener(() -> executor.execute(listener::onComplete)); 127 } 128 129 /** 130 * Adds list complete event listener. 131 */ addOnCompleteListener(@onNull OnCompleteListener listener)132 public void addOnCompleteListener(@NonNull OnCompleteListener listener) { 133 synchronized (mLock) { 134 if (mIsClosed) return; 135 mOnCompleteListeners.add(Objects.requireNonNull(listener)); 136 if (mIsComplete) listener.onComplete(); 137 } 138 } 139 140 /** 141 * Removes list complete event listener. 142 */ removeOnCompleteListener(@onNull OnCompleteListener listener)143 public void removeOnCompleteListener(@NonNull OnCompleteListener listener) { 144 synchronized (mLock) { 145 if (mIsClosed) return; 146 mOnCompleteListeners.remove(Objects.requireNonNull(listener)); 147 } 148 } 149 setOnCloseListener(@ullable OnCloseListener listener)150 void setOnCloseListener(@Nullable OnCloseListener listener) { 151 synchronized (mLock) { 152 if (mOnCloseListener != null) { 153 throw new IllegalStateException("Close callback is already set"); 154 } 155 mOnCloseListener = listener; 156 } 157 } 158 159 /** 160 * Disables list updates and releases all resources. 161 */ close()162 public void close() { 163 synchronized (mLock) { 164 if (mIsClosed) return; 165 mIsClosed = true; 166 mPrograms.clear(); 167 mListCallbacks.clear(); 168 mOnCompleteListeners.clear(); 169 if (mOnCloseListener != null) { 170 mOnCloseListener.onClose(); 171 mOnCloseListener = null; 172 } 173 } 174 } 175 apply(@onNull Chunk chunk)176 void apply(@NonNull Chunk chunk) { 177 synchronized (mLock) { 178 if (mIsClosed) return; 179 180 mIsComplete = false; 181 182 if (chunk.isPurge()) { 183 new HashSet<>(mPrograms.keySet()).stream().forEach(id -> removeLocked(id)); 184 } 185 186 chunk.getRemoved().stream().forEach(id -> removeLocked(id)); 187 chunk.getModified().stream().forEach(info -> putLocked(info)); 188 189 if (chunk.isComplete()) { 190 mIsComplete = true; 191 mOnCompleteListeners.forEach(cb -> cb.onComplete()); 192 } 193 } 194 } 195 putLocked(@onNull RadioManager.ProgramInfo value)196 private void putLocked(@NonNull RadioManager.ProgramInfo value) { 197 ProgramSelector.Identifier key = value.getSelector().getPrimaryId(); 198 mPrograms.put(Objects.requireNonNull(key), value); 199 ProgramSelector.Identifier sel = value.getSelector().getPrimaryId(); 200 mListCallbacks.forEach(cb -> cb.onItemChanged(sel)); 201 } 202 removeLocked(@onNull ProgramSelector.Identifier key)203 private void removeLocked(@NonNull ProgramSelector.Identifier key) { 204 RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key)); 205 if (removed == null) return; 206 ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId(); 207 mListCallbacks.forEach(cb -> cb.onItemRemoved(sel)); 208 } 209 210 /** 211 * Converts the program list in its current shape to the static List<>. 212 * 213 * @return the new List<> object; it won't receive any further updates 214 */ toList()215 public @NonNull List<RadioManager.ProgramInfo> toList() { 216 synchronized (mLock) { 217 return mPrograms.values().stream().collect(Collectors.toList()); 218 } 219 } 220 221 /** 222 * Returns the program with a specified primary identifier. 223 * 224 * @param id primary identifier of a program to fetch 225 * @return the program info, or null if there is no such program on the list 226 */ get(@onNull ProgramSelector.Identifier id)227 public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) { 228 synchronized (mLock) { 229 return mPrograms.get(Objects.requireNonNull(id)); 230 } 231 } 232 233 /** 234 * Filter for the program list. 235 */ 236 public static final class Filter implements Parcelable { 237 private final @NonNull Set<Integer> mIdentifierTypes; 238 private final @NonNull Set<ProgramSelector.Identifier> mIdentifiers; 239 private final boolean mIncludeCategories; 240 private final boolean mExcludeModifications; 241 private final @Nullable Map<String, String> mVendorFilter; 242 243 /** 244 * Constructor of program list filter. 245 * 246 * Arrays passed to this constructor become owned by this object, do not modify them later. 247 * 248 * @param identifierTypes see getIdentifierTypes() 249 * @param identifiers see getIdentifiers() 250 * @param includeCategories see areCategoriesIncluded() 251 * @param excludeModifications see areModificationsExcluded() 252 */ Filter(@onNull Set<Integer> identifierTypes, @NonNull Set<ProgramSelector.Identifier> identifiers, boolean includeCategories, boolean excludeModifications)253 public Filter(@NonNull Set<Integer> identifierTypes, 254 @NonNull Set<ProgramSelector.Identifier> identifiers, 255 boolean includeCategories, boolean excludeModifications) { 256 mIdentifierTypes = Objects.requireNonNull(identifierTypes); 257 mIdentifiers = Objects.requireNonNull(identifiers); 258 mIncludeCategories = includeCategories; 259 mExcludeModifications = excludeModifications; 260 mVendorFilter = null; 261 } 262 263 /** 264 * @hide for framework use only 265 */ Filter()266 public Filter() { 267 mIdentifierTypes = Collections.emptySet(); 268 mIdentifiers = Collections.emptySet(); 269 mIncludeCategories = false; 270 mExcludeModifications = false; 271 mVendorFilter = null; 272 } 273 274 /** 275 * @hide for framework use only 276 */ Filter(@ullable Map<String, String> vendorFilter)277 public Filter(@Nullable Map<String, String> vendorFilter) { 278 mIdentifierTypes = Collections.emptySet(); 279 mIdentifiers = Collections.emptySet(); 280 mIncludeCategories = false; 281 mExcludeModifications = false; 282 mVendorFilter = vendorFilter; 283 } 284 Filter(@onNull Parcel in)285 private Filter(@NonNull Parcel in) { 286 mIdentifierTypes = Utils.createIntSet(in); 287 mIdentifiers = Utils.createSet(in, ProgramSelector.Identifier.CREATOR); 288 mIncludeCategories = in.readByte() != 0; 289 mExcludeModifications = in.readByte() != 0; 290 mVendorFilter = Utils.readStringMap(in); 291 } 292 293 @Override writeToParcel(Parcel dest, int flags)294 public void writeToParcel(Parcel dest, int flags) { 295 Utils.writeIntSet(dest, mIdentifierTypes); 296 Utils.writeSet(dest, mIdentifiers); 297 dest.writeByte((byte) (mIncludeCategories ? 1 : 0)); 298 dest.writeByte((byte) (mExcludeModifications ? 1 : 0)); 299 Utils.writeStringMap(dest, mVendorFilter); 300 } 301 302 @Override describeContents()303 public int describeContents() { 304 return 0; 305 } 306 307 public static final @android.annotation.NonNull Parcelable.Creator<Filter> CREATOR = new Parcelable.Creator<Filter>() { 308 public Filter createFromParcel(Parcel in) { 309 return new Filter(in); 310 } 311 312 public Filter[] newArray(int size) { 313 return new Filter[size]; 314 } 315 }; 316 317 /** 318 * @hide for framework use only 319 */ getVendorFilter()320 public Map<String, String> getVendorFilter() { 321 return mVendorFilter; 322 } 323 324 /** 325 * Returns the list of identifier types that satisfy the filter. 326 * 327 * If the program list entry contains at least one identifier of the type 328 * listed, it satisfies this condition. 329 * 330 * Empty list means no filtering on identifier type. 331 * 332 * @return the list of accepted identifier types, must not be modified 333 */ getIdentifierTypes()334 public @NonNull Set<Integer> getIdentifierTypes() { 335 return mIdentifierTypes; 336 } 337 338 /** 339 * Returns the list of identifiers that satisfy the filter. 340 * 341 * If the program list entry contains at least one listed identifier, 342 * it satisfies this condition. 343 * 344 * Empty list means no filtering on identifier. 345 * 346 * @return the list of accepted identifiers, must not be modified 347 */ getIdentifiers()348 public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() { 349 return mIdentifiers; 350 } 351 352 /** 353 * Checks, if non-tunable entries that define tree structure on the 354 * program list (i.e. DAB ensembles) should be included. 355 * 356 * @see {@link ProgramSelector.Identifier#isCategory()} 357 */ areCategoriesIncluded()358 public boolean areCategoriesIncluded() { 359 return mIncludeCategories; 360 } 361 362 /** 363 * Checks, if updates on entry modifications should be disabled. 364 * 365 * If true, 'modified' vector of ProgramListChunk must contain list 366 * additions only. Once the program is added to the list, it's not 367 * updated anymore. 368 */ areModificationsExcluded()369 public boolean areModificationsExcluded() { 370 return mExcludeModifications; 371 } 372 373 @Override hashCode()374 public int hashCode() { 375 return Objects.hash(mIdentifierTypes, mIdentifiers, mIncludeCategories, 376 mExcludeModifications); 377 } 378 379 @Override equals(@ullable Object obj)380 public boolean equals(@Nullable Object obj) { 381 if (this == obj) return true; 382 if (!(obj instanceof Filter)) return false; 383 Filter other = (Filter) obj; 384 385 if (mIncludeCategories != other.mIncludeCategories) return false; 386 if (mExcludeModifications != other.mExcludeModifications) return false; 387 if (!Objects.equals(mIdentifierTypes, other.mIdentifierTypes)) return false; 388 if (!Objects.equals(mIdentifiers, other.mIdentifiers)) return false; 389 return true; 390 } 391 392 @NonNull 393 @Override toString()394 public String toString() { 395 return "Filter [mIdentifierTypes=" + mIdentifierTypes 396 + ", mIdentifiers=" + mIdentifiers 397 + ", mIncludeCategories=" + mIncludeCategories 398 + ", mExcludeModifications=" + mExcludeModifications + "]"; 399 } 400 } 401 402 /** 403 * @hide This is a transport class used for internal communication between 404 * Broadcast Radio Service and RadioManager. 405 * Do not use it directly. 406 */ 407 public static final class Chunk implements Parcelable { 408 private final boolean mPurge; 409 private final boolean mComplete; 410 private final @NonNull Set<RadioManager.ProgramInfo> mModified; 411 private final @NonNull Set<ProgramSelector.Identifier> mRemoved; 412 Chunk(boolean purge, boolean complete, @Nullable Set<RadioManager.ProgramInfo> modified, @Nullable Set<ProgramSelector.Identifier> removed)413 public Chunk(boolean purge, boolean complete, 414 @Nullable Set<RadioManager.ProgramInfo> modified, 415 @Nullable Set<ProgramSelector.Identifier> removed) { 416 mPurge = purge; 417 mComplete = complete; 418 mModified = (modified != null) ? modified : Collections.emptySet(); 419 mRemoved = (removed != null) ? removed : Collections.emptySet(); 420 } 421 Chunk(@onNull Parcel in)422 private Chunk(@NonNull Parcel in) { 423 mPurge = in.readByte() != 0; 424 mComplete = in.readByte() != 0; 425 mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR); 426 mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR); 427 } 428 429 @Override writeToParcel(Parcel dest, int flags)430 public void writeToParcel(Parcel dest, int flags) { 431 dest.writeByte((byte) (mPurge ? 1 : 0)); 432 dest.writeByte((byte) (mComplete ? 1 : 0)); 433 Utils.writeSet(dest, mModified); 434 Utils.writeSet(dest, mRemoved); 435 } 436 437 @Override describeContents()438 public int describeContents() { 439 return 0; 440 } 441 442 public static final @android.annotation.NonNull Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() { 443 public Chunk createFromParcel(Parcel in) { 444 return new Chunk(in); 445 } 446 447 public Chunk[] newArray(int size) { 448 return new Chunk[size]; 449 } 450 }; 451 isPurge()452 public boolean isPurge() { 453 return mPurge; 454 } 455 isComplete()456 public boolean isComplete() { 457 return mComplete; 458 } 459 getModified()460 public @NonNull Set<RadioManager.ProgramInfo> getModified() { 461 return mModified; 462 } 463 getRemoved()464 public @NonNull Set<ProgramSelector.Identifier> getRemoved() { 465 return mRemoved; 466 } 467 468 @Override equals(Object obj)469 public boolean equals(Object obj) { 470 if (this == obj) return true; 471 if (!(obj instanceof Chunk)) return false; 472 Chunk other = (Chunk) obj; 473 474 if (mPurge != other.mPurge) return false; 475 if (mComplete != other.mComplete) return false; 476 if (!Objects.equals(mModified, other.mModified)) return false; 477 if (!Objects.equals(mRemoved, other.mRemoved)) return false; 478 return true; 479 } 480 481 @Override toString()482 public String toString() { 483 return "Chunk [mPurge=" + mPurge + ", mComplete=" + mComplete 484 + ", mModified=" + mModified + ", mRemoved=" + mRemoved + "]"; 485 } 486 } 487 } 488