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.FlaggedApi; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.SystemApi; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.util.ArrayMap; 27 import android.util.ArraySet; 28 29 import com.android.internal.annotations.GuardedBy; 30 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.Iterator; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Objects; 37 import java.util.Set; 38 import java.util.concurrent.Executor; 39 40 /** 41 * @hide 42 */ 43 @SystemApi 44 public final class ProgramList implements AutoCloseable { 45 46 private final Object mLock = new Object(); 47 48 @GuardedBy("mLock") 49 private final ArrayMap<ProgramSelector.Identifier, ArrayMap<UniqueProgramIdentifier, 50 RadioManager.ProgramInfo>> mPrograms = new ArrayMap<>(); 51 52 @GuardedBy("mLock") 53 private final List<ListCallback> mListCallbacks = new ArrayList<>(); 54 55 @GuardedBy("mLock") 56 private final List<OnCompleteListener> mOnCompleteListeners = new ArrayList<>(); 57 58 @GuardedBy("mLock") 59 private OnCloseListener mOnCloseListener; 60 61 @GuardedBy("mLock") 62 private boolean mIsClosed; 63 64 @GuardedBy("mLock") 65 private boolean mIsComplete; 66 ProgramList()67 ProgramList() {} 68 69 /** 70 * Callback for list change operations. 71 */ 72 public abstract static class ListCallback { 73 /** 74 * Called when item was modified or added to the list. 75 */ onItemChanged(@onNull ProgramSelector.Identifier id)76 public void onItemChanged(@NonNull ProgramSelector.Identifier id) { } 77 78 /** 79 * Called when item was removed from the list. 80 */ onItemRemoved(@onNull ProgramSelector.Identifier id)81 public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { } 82 } 83 84 /** 85 * Listener of list complete event. 86 */ 87 public interface OnCompleteListener { 88 /** 89 * Called when the list turned complete (i.e. when the scan process 90 * came to an end). 91 */ onComplete()92 void onComplete(); 93 } 94 95 interface OnCloseListener { onClose()96 void onClose(); 97 } 98 99 /** 100 * Registers list change callback with executor. 101 */ registerListCallback(@onNull @allbackExecutor Executor executor, @NonNull ListCallback callback)102 public void registerListCallback(@NonNull @CallbackExecutor Executor executor, 103 @NonNull ListCallback callback) { 104 registerListCallback(new ListCallback() { 105 public void onItemChanged(@NonNull ProgramSelector.Identifier id) { 106 executor.execute(() -> callback.onItemChanged(id)); 107 } 108 109 public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { 110 executor.execute(() -> callback.onItemRemoved(id)); 111 } 112 }); 113 } 114 115 /** 116 * Registers list change callback. 117 */ registerListCallback(@onNull ListCallback callback)118 public void registerListCallback(@NonNull ListCallback callback) { 119 synchronized (mLock) { 120 if (mIsClosed) return; 121 mListCallbacks.add(Objects.requireNonNull(callback)); 122 } 123 } 124 125 /** 126 * Unregisters list change callback. 127 */ unregisterListCallback(@onNull ListCallback callback)128 public void unregisterListCallback(@NonNull ListCallback callback) { 129 synchronized (mLock) { 130 if (mIsClosed) return; 131 mListCallbacks.remove(Objects.requireNonNull(callback)); 132 } 133 } 134 135 /** 136 * Adds list complete event listener with executor. 137 */ addOnCompleteListener(@onNull @allbackExecutor Executor executor, @NonNull OnCompleteListener listener)138 public void addOnCompleteListener(@NonNull @CallbackExecutor Executor executor, 139 @NonNull OnCompleteListener listener) { 140 addOnCompleteListener(() -> executor.execute(listener::onComplete)); 141 } 142 143 /** 144 * Adds list complete event listener. 145 */ addOnCompleteListener(@onNull OnCompleteListener listener)146 public void addOnCompleteListener(@NonNull OnCompleteListener listener) { 147 synchronized (mLock) { 148 if (mIsClosed) return; 149 mOnCompleteListeners.add(Objects.requireNonNull(listener)); 150 if (mIsComplete) listener.onComplete(); 151 } 152 } 153 154 /** 155 * Removes list complete event listener. 156 */ removeOnCompleteListener(@onNull OnCompleteListener listener)157 public void removeOnCompleteListener(@NonNull OnCompleteListener listener) { 158 synchronized (mLock) { 159 if (mIsClosed) return; 160 mOnCompleteListeners.remove(Objects.requireNonNull(listener)); 161 } 162 } 163 setOnCloseListener(@ullable OnCloseListener listener)164 void setOnCloseListener(@Nullable OnCloseListener listener) { 165 synchronized (mLock) { 166 if (mOnCloseListener != null) { 167 throw new IllegalStateException("Close callback is already set"); 168 } 169 mOnCloseListener = listener; 170 } 171 } 172 173 /** 174 * Disables list updates and releases all resources. 175 */ close()176 public void close() { 177 OnCloseListener onCompleteListenersCopied = null; 178 synchronized (mLock) { 179 if (mIsClosed) return; 180 mIsClosed = true; 181 mPrograms.clear(); 182 mListCallbacks.clear(); 183 mOnCompleteListeners.clear(); 184 if (mOnCloseListener != null) { 185 onCompleteListenersCopied = mOnCloseListener; 186 mOnCloseListener = null; 187 } 188 } 189 190 if (onCompleteListenersCopied != null) { 191 onCompleteListenersCopied.onClose(); 192 } 193 } 194 apply(Chunk chunk)195 void apply(Chunk chunk) { 196 List<ProgramSelector.Identifier> removedList = new ArrayList<>(); 197 Set<ProgramSelector.Identifier> changedSet = new ArraySet<>(); 198 List<ProgramList.ListCallback> listCallbacksCopied; 199 List<OnCompleteListener> onCompleteListenersCopied = new ArrayList<>(); 200 synchronized (mLock) { 201 if (mIsClosed) return; 202 203 mIsComplete = false; 204 listCallbacksCopied = new ArrayList<>(mListCallbacks); 205 206 if (chunk.isPurge()) { 207 Iterator<Map.Entry<ProgramSelector.Identifier, 208 ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo>>> 209 programsIterator = mPrograms.entrySet().iterator(); 210 while (programsIterator.hasNext()) { 211 Map.Entry<ProgramSelector.Identifier, ArrayMap<UniqueProgramIdentifier, 212 RadioManager.ProgramInfo>> removed = programsIterator.next(); 213 if (removed.getValue() != null) { 214 removedList.add(removed.getKey()); 215 } 216 programsIterator.remove(); 217 } 218 } 219 220 Iterator<UniqueProgramIdentifier> removedIterator = chunk.getRemoved().iterator(); 221 while (removedIterator.hasNext()) { 222 removeLocked(removedIterator.next(), removedList); 223 } 224 Iterator<RadioManager.ProgramInfo> modifiedIterator = chunk.getModified().iterator(); 225 while (modifiedIterator.hasNext()) { 226 putLocked(modifiedIterator.next(), changedSet); 227 } 228 229 if (chunk.isComplete()) { 230 mIsComplete = true; 231 onCompleteListenersCopied = new ArrayList<>(mOnCompleteListeners); 232 } 233 } 234 235 for (int i = 0; i < removedList.size(); i++) { 236 for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) { 237 listCallbacksCopied.get(cbIndex).onItemRemoved(removedList.get(i)); 238 } 239 } 240 Iterator<ProgramSelector.Identifier> changedIterator = changedSet.iterator(); 241 while (changedIterator.hasNext()) { 242 ProgramSelector.Identifier changedId = changedIterator.next(); 243 for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) { 244 listCallbacksCopied.get(cbIndex).onItemChanged(changedId); 245 } 246 } 247 if (chunk.isComplete()) { 248 for (int cbIndex = 0; cbIndex < onCompleteListenersCopied.size(); cbIndex++) { 249 onCompleteListenersCopied.get(cbIndex).onComplete(); 250 } 251 } 252 } 253 254 @GuardedBy("mLock") putLocked(RadioManager.ProgramInfo value, Set<ProgramSelector.Identifier> changedIdentifierSet)255 private void putLocked(RadioManager.ProgramInfo value, 256 Set<ProgramSelector.Identifier> changedIdentifierSet) { 257 UniqueProgramIdentifier key = new UniqueProgramIdentifier( 258 value.getSelector()); 259 ProgramSelector.Identifier primaryKey = Objects.requireNonNull(key.getPrimaryId()); 260 if (!mPrograms.containsKey(primaryKey)) { 261 mPrograms.put(primaryKey, new ArrayMap<>()); 262 } 263 mPrograms.get(primaryKey).put(key, value); 264 changedIdentifierSet.add(primaryKey); 265 } 266 267 @GuardedBy("mLock") removeLocked(UniqueProgramIdentifier key, List<ProgramSelector.Identifier> removedIdentifierList)268 private void removeLocked(UniqueProgramIdentifier key, 269 List<ProgramSelector.Identifier> removedIdentifierList) { 270 ProgramSelector.Identifier primaryKey = Objects.requireNonNull(key.getPrimaryId()); 271 if (!mPrograms.containsKey(primaryKey)) { 272 return; 273 } 274 Map<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries = mPrograms.get(primaryKey); 275 RadioManager.ProgramInfo removed = entries.remove(Objects.requireNonNull(key)); 276 if (removed == null) return; 277 if (entries.size() == 0) { 278 removedIdentifierList.add(primaryKey); 279 } 280 } 281 282 /** 283 * Converts the program list in its current shape to the static List<>. 284 * 285 * @return the new List<> object; it won't receive any further updates 286 */ toList()287 public @NonNull List<RadioManager.ProgramInfo> toList() { 288 List<RadioManager.ProgramInfo> list = new ArrayList<>(); 289 synchronized (mLock) { 290 for (int index = 0; index < mPrograms.size(); index++) { 291 ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries = 292 mPrograms.valueAt(index); 293 list.addAll(entries.values()); 294 } 295 } 296 return list; 297 } 298 299 /** 300 * Returns the program with a specified primary identifier. 301 * 302 * <p>This method only returns the first program from the list return from 303 * {@link #getProgramInfos} 304 * 305 * @param id primary identifier of a program to fetch 306 * @return the program info, or null if there is no such program on the list 307 * 308 * @deprecated Use {@link #getProgramInfos(ProgramSelector.Identifier)} to get all programs 309 * with the given primary identifier 310 */ 311 @Deprecated get(@onNull ProgramSelector.Identifier id)312 public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) { 313 Map<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries; 314 synchronized (mLock) { 315 entries = mPrograms.get(Objects.requireNonNull(id, 316 "Primary identifier can not be null")); 317 } 318 if (entries == null) { 319 return null; 320 } 321 return entries.entrySet().iterator().next().getValue(); 322 } 323 324 /** 325 * Returns the program list with a specified primary identifier. 326 * 327 * @param id primary identifier of a program to fetch 328 * @return the program info list with the primary identifier, or empty list if there is no such 329 * program identifier on the list 330 * @throws NullPointerException if primary identifier is {@code null} 331 */ 332 @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED) getProgramInfos( @onNull ProgramSelector.Identifier id)333 public @NonNull List<RadioManager.ProgramInfo> getProgramInfos( 334 @NonNull ProgramSelector.Identifier id) { 335 Objects.requireNonNull(id, "Primary identifier can not be null"); 336 ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries; 337 synchronized (mLock) { 338 entries = mPrograms.get(id); 339 } 340 341 if (entries == null) { 342 return new ArrayList<>(); 343 } 344 return new ArrayList<>(entries.values()); 345 } 346 347 /** 348 * Filter for the program list. 349 */ 350 public static final class Filter implements Parcelable { 351 private final @NonNull Set<Integer> mIdentifierTypes; 352 private final @NonNull Set<ProgramSelector.Identifier> mIdentifiers; 353 private final boolean mIncludeCategories; 354 private final boolean mExcludeModifications; 355 private final @Nullable Map<String, String> mVendorFilter; 356 357 /** 358 * Constructor of program list filter. 359 * 360 * <p>Arrays passed to this constructor will be owned by this object, do not modify them. 361 * 362 * @param identifierTypes see getIdentifierTypes() 363 * @param identifiers see getIdentifiers() 364 * @param includeCategories see areCategoriesIncluded() 365 * @param excludeModifications see areModificationsExcluded() 366 */ Filter(@onNull Set<Integer> identifierTypes, @NonNull Set<ProgramSelector.Identifier> identifiers, boolean includeCategories, boolean excludeModifications)367 public Filter(@NonNull Set<Integer> identifierTypes, 368 @NonNull Set<ProgramSelector.Identifier> identifiers, 369 boolean includeCategories, boolean excludeModifications) { 370 mIdentifierTypes = Objects.requireNonNull(identifierTypes); 371 mIdentifiers = Objects.requireNonNull(identifiers); 372 mIncludeCategories = includeCategories; 373 mExcludeModifications = excludeModifications; 374 mVendorFilter = null; 375 } 376 377 /** 378 * @hide for framework use only 379 */ Filter()380 public Filter() { 381 mIdentifierTypes = Collections.emptySet(); 382 mIdentifiers = Collections.emptySet(); 383 mIncludeCategories = false; 384 mExcludeModifications = false; 385 mVendorFilter = null; 386 } 387 388 /** 389 * @hide for framework use only 390 */ Filter(@ullable Map<String, String> vendorFilter)391 public Filter(@Nullable Map<String, String> vendorFilter) { 392 mIdentifierTypes = Collections.emptySet(); 393 mIdentifiers = Collections.emptySet(); 394 mIncludeCategories = false; 395 mExcludeModifications = false; 396 mVendorFilter = vendorFilter; 397 } 398 Filter(@onNull Parcel in)399 private Filter(@NonNull Parcel in) { 400 mIdentifierTypes = Utils.createIntSet(in); 401 mIdentifiers = Utils.createSet(in, ProgramSelector.Identifier.CREATOR); 402 mIncludeCategories = in.readByte() != 0; 403 mExcludeModifications = in.readByte() != 0; 404 mVendorFilter = Utils.readStringMap(in); 405 } 406 407 @Override writeToParcel(Parcel dest, int flags)408 public void writeToParcel(Parcel dest, int flags) { 409 Utils.writeIntSet(dest, mIdentifierTypes); 410 Utils.writeSet(dest, mIdentifiers); 411 dest.writeByte((byte) (mIncludeCategories ? 1 : 0)); 412 dest.writeByte((byte) (mExcludeModifications ? 1 : 0)); 413 Utils.writeStringMap(dest, mVendorFilter); 414 } 415 416 @Override describeContents()417 public int describeContents() { 418 return 0; 419 } 420 421 public static final @android.annotation.NonNull Parcelable.Creator<Filter> CREATOR = new Parcelable.Creator<Filter>() { 422 public Filter createFromParcel(Parcel in) { 423 return new Filter(in); 424 } 425 426 public Filter[] newArray(int size) { 427 return new Filter[size]; 428 } 429 }; 430 431 /** 432 * @hide for framework use only 433 */ getVendorFilter()434 public Map<String, String> getVendorFilter() { 435 return mVendorFilter; 436 } 437 438 /** 439 * Returns the list of identifier types that satisfy the filter. 440 * 441 * <p>If the program list entry contains at least one identifier of the type 442 * listed, it satisfies this condition. Empty list means no filtering on 443 * identifier type. 444 * 445 * @return the set of accepted identifier types, must not be modified 446 */ getIdentifierTypes()447 public @NonNull Set<Integer> getIdentifierTypes() { 448 return mIdentifierTypes; 449 } 450 451 /** 452 * Returns the list of identifiers that satisfy the filter. 453 * 454 * <p>If the program list entry contains at least one listed identifier, 455 * it satisfies this condition. Empty list means no filtering on identifier. 456 * 457 * @return the set of accepted identifiers, must not be modified 458 */ getIdentifiers()459 public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() { 460 return mIdentifiers; 461 } 462 463 /** 464 * Checks, if non-tunable entries that define tree structure on the 465 * program list (i.e. DAB ensembles) should be included. 466 * 467 * @see ProgramSelector.Identifier#isCategoryType() 468 */ areCategoriesIncluded()469 public boolean areCategoriesIncluded() { 470 return mIncludeCategories; 471 } 472 473 /** 474 * Checks, if updates on entry modifications should be disabled. 475 * 476 * <p>If true, 'modified' vector of ProgramListChunk must contain list 477 * additions only. Once the program is added to the list, it's not 478 * updated anymore. 479 */ areModificationsExcluded()480 public boolean areModificationsExcluded() { 481 return mExcludeModifications; 482 } 483 484 @Override hashCode()485 public int hashCode() { 486 return Objects.hash(mIdentifierTypes, mIdentifiers, mIncludeCategories, 487 mExcludeModifications); 488 } 489 490 @Override equals(@ullable Object obj)491 public boolean equals(@Nullable Object obj) { 492 if (this == obj) return true; 493 if (!(obj instanceof Filter)) return false; 494 Filter other = (Filter) obj; 495 496 if (mIncludeCategories != other.mIncludeCategories) return false; 497 if (mExcludeModifications != other.mExcludeModifications) return false; 498 if (!Objects.equals(mIdentifierTypes, other.mIdentifierTypes)) return false; 499 if (!Objects.equals(mIdentifiers, other.mIdentifiers)) return false; 500 return true; 501 } 502 503 @NonNull 504 @Override toString()505 public String toString() { 506 return "Filter [mIdentifierTypes=" + mIdentifierTypes 507 + ", mIdentifiers=" + mIdentifiers 508 + ", mIncludeCategories=" + mIncludeCategories 509 + ", mExcludeModifications=" + mExcludeModifications + "]"; 510 } 511 } 512 513 /** 514 * @hide This is a transport class used for internal communication between 515 * Broadcast Radio Service and RadioManager. 516 * Do not use it directly. 517 */ 518 public static final class Chunk implements Parcelable { 519 private final boolean mPurge; 520 private final boolean mComplete; 521 private final @NonNull Set<RadioManager.ProgramInfo> mModified; 522 private final @NonNull Set<UniqueProgramIdentifier> mRemoved; 523 Chunk(boolean purge, boolean complete, @Nullable Set<RadioManager.ProgramInfo> modified, @Nullable Set<UniqueProgramIdentifier> removed)524 public Chunk(boolean purge, boolean complete, 525 @Nullable Set<RadioManager.ProgramInfo> modified, 526 @Nullable Set<UniqueProgramIdentifier> removed) { 527 mPurge = purge; 528 mComplete = complete; 529 mModified = (modified != null) ? modified : Collections.emptySet(); 530 mRemoved = (removed != null) ? removed : Collections.emptySet(); 531 } 532 Chunk(@onNull Parcel in)533 private Chunk(@NonNull Parcel in) { 534 mPurge = in.readByte() != 0; 535 mComplete = in.readByte() != 0; 536 mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR); 537 mRemoved = Utils.createSet(in, UniqueProgramIdentifier.CREATOR); 538 } 539 540 @Override writeToParcel(Parcel dest, int flags)541 public void writeToParcel(Parcel dest, int flags) { 542 dest.writeByte((byte) (mPurge ? 1 : 0)); 543 dest.writeByte((byte) (mComplete ? 1 : 0)); 544 Utils.writeSet(dest, mModified); 545 Utils.writeSet(dest, mRemoved); 546 } 547 548 @Override describeContents()549 public int describeContents() { 550 return 0; 551 } 552 553 public static final @android.annotation.NonNull Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() { 554 public Chunk createFromParcel(Parcel in) { 555 return new Chunk(in); 556 } 557 558 public Chunk[] newArray(int size) { 559 return new Chunk[size]; 560 } 561 }; 562 isPurge()563 public boolean isPurge() { 564 return mPurge; 565 } 566 isComplete()567 public boolean isComplete() { 568 return mComplete; 569 } 570 getModified()571 public @NonNull Set<RadioManager.ProgramInfo> getModified() { 572 return mModified; 573 } 574 getRemoved()575 public @NonNull Set<UniqueProgramIdentifier> getRemoved() { 576 return mRemoved; 577 } 578 579 @Override equals(@ullable Object obj)580 public boolean equals(@Nullable Object obj) { 581 if (this == obj) return true; 582 if (!(obj instanceof Chunk)) return false; 583 Chunk other = (Chunk) obj; 584 585 if (mPurge != other.mPurge) return false; 586 if (mComplete != other.mComplete) return false; 587 if (!Objects.equals(mModified, other.mModified)) return false; 588 if (!Objects.equals(mRemoved, other.mRemoved)) return false; 589 return true; 590 } 591 592 @Override toString()593 public String toString() { 594 return "Chunk [mPurge=" + mPurge + ", mComplete=" + mComplete 595 + ", mModified=" + mModified + ", mRemoved=" + mRemoved + "]"; 596 } 597 } 598 } 599